···11+{ config, lib, pkgs, services, ... }:
22+with lib;
33+44+let
55+ cfg = config.services.journalwatch;
66+ user = "journalwatch";
77+ dataDir = "/var/lib/${user}";
88+99+ journalwatchConfig = pkgs.writeText "config" (''
1010+ # (File Generated by NixOS journalwatch module.)
1111+ [DEFAULT]
1212+ mail_binary = ${cfg.mailBinary}
1313+ priority = ${toString cfg.priority}
1414+ mail_from = ${cfg.mailFrom}
1515+ ''
1616+ + optionalString (cfg.mailTo != null) ''
1717+ mail_to = ${cfg.mailTo}
1818+ ''
1919+ + cfg.extraConfig);
2020+2121+ journalwatchPatterns = pkgs.writeText "patterns" ''
2222+ # (File Generated by NixOS journalwatch module.)
2323+2424+ ${mkPatterns cfg.filterBlocks}
2525+ '';
2626+2727+ # empty line at the end needed to to separate the blocks
2828+ mkPatterns = filterBlocks: concatStringsSep "\n" (map (block: ''
2929+ ${block.match}
3030+ ${block.filters}
3131+3232+ '') filterBlocks);
3333+3434+3535+in {
3636+ options = {
3737+ services.journalwatch = {
3838+ enable = mkOption {
3939+ type = types.bool;
4040+ default = false;
4141+ description = ''
4242+ If enabled, periodically check the journal with journalwatch and report the results by mail.
4343+ '';
4444+ };
4545+4646+ priority = mkOption {
4747+ type = types.int;
4848+ default = 6;
4949+ description = ''
5050+ Lowest priority of message to be considered.
5151+ A value between 7 ("debug"), and 0 ("emerg"). Defaults to 6 ("info").
5252+ If you don't care about anything with "info" priority, you can reduce
5353+ this to e.g. 5 ("notice") to considerably reduce the amount of
5454+ messages without needing many <option>filterBlocks</option>.
5555+ '';
5656+ };
5757+5858+ # HACK: this is a workaround for journalwatch's usage of socket.getfqdn() which always returns localhost if
5959+ # there's an alias for the localhost on a separate line in /etc/hosts, or take for ages if it's not present and
6060+ # then return something right-ish in the direction of /etc/hostname. Just bypass it completely.
6161+ mailFrom = mkOption {
6262+ type = types.str;
6363+ default = "journalwatch@${config.networking.hostName}";
6464+ description = ''
6565+ Mail address to send journalwatch reports from.
6666+ '';
6767+ };
6868+6969+ mailTo = mkOption {
7070+ type = types.nullOr types.str;
7171+ default = null;
7272+ description = ''
7373+ Mail address to send journalwatch reports to.
7474+ '';
7575+ };
7676+7777+ mailBinary = mkOption {
7878+ type = types.path;
7979+ default = "/run/wrappers/bin/sendmail";
8080+ description = ''
8181+ Sendmail-compatible binary to be used to send the messages.
8282+ '';
8383+ };
8484+8585+ extraConfig = mkOption {
8686+ type = types.str;
8787+ default = "";
8888+ description = ''
8989+ Extra lines to be added verbatim to the journalwatch/config configuration file.
9090+ You can add any commandline argument to the config, without the '--'.
9191+ See <literal>journalwatch --help</literal> for all arguments and their description.
9292+ '';
9393+ };
9494+9595+ filterBlocks = mkOption {
9696+ type = types.listOf (types.submodule {
9797+ options = {
9898+ match = mkOption {
9999+ type = types.str;
100100+ example = "SYSLOG_IDENTIFIER = systemd";
101101+ description = ''
102102+ Syntax: <literal>field = value</literal>
103103+ Specifies the log entry <literal>field</literal> this block should apply to.
104104+ If the <literal>field</literal> of a message matches this <literal>value</literal>,
105105+ this patternBlock's <option>filters</option> are applied.
106106+ If <literal>value</literal> starts and ends with a slash, it is interpreted as
107107+ an extended python regular expression, if not, it's an exact match.
108108+ The journal fields are explained in systemd.journal-fields(7).
109109+ '';
110110+ };
111111+112112+ filters = mkOption {
113113+ type = types.str;
114114+ example = ''
115115+ (Stopped|Stopping|Starting|Started) .*
116116+ (Reached target|Stopped target) .*
117117+ '';
118118+ description = ''
119119+ The filters to apply on all messages which satisfy <option>match</option>.
120120+ Any of those messages that match any specified filter will be removed from journalwatch's output.
121121+ Each filter is an extended Python regular expression.
122122+ You can specify multiple filters and separate them by newlines.
123123+ Lines starting with '#' are comments. Inline-comments are not permitted.
124124+ '';
125125+ };
126126+ };
127127+ });
128128+129129+ example = [
130130+ # examples taken from upstream
131131+ {
132132+ match = "_SYSTEMD_UNIT = systemd-logind.service";
133133+ filters = ''
134134+ New session [a-z]?\d+ of user \w+\.
135135+ Removed session [a-z]?\d+\.
136136+ '';
137137+ }
138138+139139+ {
140140+ match = "SYSLOG_IDENTIFIER = /(CROND|crond)/";
141141+ filters = ''
142142+ pam_unix\(crond:session\): session (opened|closed) for user \w+
143143+ \(\w+\) CMD .*
144144+ '';
145145+ }
146146+ ];
147147+148148+ # another example from upstream.
149149+ # very useful on priority = 6, and required as journalwatch throws an error when no pattern is defined at all.
150150+ default = [
151151+ {
152152+ match = "SYSLOG_IDENTIFIER = systemd";
153153+ filters = ''
154154+ (Stopped|Stopping|Starting|Started) .*
155155+ (Created slice|Removed slice) user-\d*\.slice\.
156156+ Received SIGRTMIN\+24 from PID .*
157157+ (Reached target|Stopped target) .*
158158+ Startup finished in \d*ms\.
159159+ '';
160160+ }
161161+ ];
162162+163163+164164+ description = ''
165165+ filterBlocks can be defined to blacklist journal messages which are not errors.
166166+ Each block matches on a log entry field, and the filters in that block then are matched
167167+ against all messages with a matching log entry field.
168168+169169+ All messages whose PRIORITY is at least 6 (INFO) are processed by journalwatch.
170170+ If you don't specify any filterBlocks, PRIORITY is reduced to 5 (NOTICE) by default.
171171+172172+ All regular expressions are extended Python regular expressions, for details
173173+ see: http://doc.pyschools.com/html/regex.html
174174+ '';
175175+ };
176176+177177+ interval = mkOption {
178178+ type = types.str;
179179+ default = "hourly";
180180+ description = ''
181181+ How often to run journalwatch.
182182+183183+ The format is described in systemd.time(7).
184184+ '';
185185+ };
186186+ accuracy = mkOption {
187187+ type = types.str;
188188+ default = "10min";
189189+ description = ''
190190+ The time window around the interval in which the journalwatch run will be scheduled.
191191+192192+ The format is described in systemd.time(7).
193193+ '';
194194+ };
195195+ };
196196+ };
197197+198198+ config = mkIf cfg.enable {
199199+200200+ users.extraUsers.${user} = {
201201+ isSystemUser = true;
202202+ createHome = true;
203203+ home = dataDir;
204204+ # for journal access
205205+ group = "systemd-journal";
206206+ };
207207+208208+ systemd.services.journalwatch = {
209209+ environment = {
210210+ XDG_DATA_HOME = "${dataDir}/share";
211211+ XDG_CONFIG_HOME = "${dataDir}/config";
212212+ };
213213+ serviceConfig = {
214214+ User = user;
215215+ Type = "oneshot";
216216+ PermissionsStartOnly = true;
217217+ ExecStart = "${pkgs.python3Packages.journalwatch}/bin/journalwatch mail";
218218+ # lowest CPU and IO priority, but both still in best-effort class to prevent starvation
219219+ Nice=19;
220220+ IOSchedulingPriority=7;
221221+ };
222222+ preStart = ''
223223+ chown -R ${user}:systemd-journal ${dataDir}
224224+ chmod -R u+rwX,go-w ${dataDir}
225225+ mkdir -p ${dataDir}/config/journalwatch
226226+ ln -sf ${journalwatchConfig} ${dataDir}/config/journalwatch/config
227227+ ln -sf ${journalwatchPatterns} ${dataDir}/config/journalwatch/patterns
228228+ '';
229229+ };
230230+231231+ systemd.timers.journalwatch = {
232232+ description = "Periodic journalwatch run";
233233+ wantedBy = [ "timers.target" ];
234234+ timerConfig = {
235235+ OnCalendar = cfg.interval;
236236+ AccuracySec = cfg.accuracy;
237237+ Persistent = true;
238238+ };
239239+ };
240240+241241+ };
242242+243243+ meta = {
244244+ maintainers = with stdenv.lib.maintainers; [ florianjacob ];
245245+ };
246246+}
+43
pkgs/tools/system/journalwatch/default.nix
···11+{ stdenv, buildPythonPackage, fetchurl, fetchgit, pythonOlder, systemd, pytest }:
22+33+buildPythonPackage rec {
44+ pname = "journalwatch";
55+ name = "${pname}-${version}";
66+ version = "1.1.0";
77+ disabled = pythonOlder "3.3";
88+99+1010+ src = fetchurl {
1111+ url = "https://github.com/The-Compiler/${pname}/archive/v${version}.tar.gz";
1212+ sha512 = "3hvbgx95hjfivz9iv0hbhj720wvm32z86vj4a60lji2zdfpbqgr2b428lvg2cpvf71l2xn6ca5v0hzyz57qylgwqzgfrx7hqhl5g38s";
1313+ };
1414+1515+ # can be removed post 1.1.0
1616+ postPatch = ''
1717+ substituteInPlace test_journalwatch.py \
1818+ --replace "U Thu Jan 1 00:00:00 1970 prio foo [1337]" "U Thu Jan 1 00:00:00 1970 pprio foo [1337]"
1919+ '';
2020+2121+2222+ doCheck = true;
2323+2424+ checkPhase = ''
2525+ pytest test_journalwatch.py
2626+ '';
2727+2828+ buildInputs = [
2929+ pytest
3030+ ];
3131+3232+ propagatedBuildInputs = [
3333+ systemd
3434+ ];
3535+3636+3737+ meta = with stdenv.lib; {
3838+ description = "journalwatch is a tool to find error messages in the systemd journal.";
3939+ homepage = "https://github.com/The-Compiler/journalwatch";
4040+ license = licenses.gpl3Plus;
4141+ maintainers = with maintainers; [ florianjacob ];
4242+ };
4343+}