grafana-alloy: don't build the frontend twice
[NixPkgs.git] / nixos / modules / services / logging / journalwatch.nix
blobab75ed656c190244b4920fd17ceb43a0e2c3c937
1 { config, lib, pkgs, ... }:
2 let
3   cfg = config.services.journalwatch;
4   user = "journalwatch";
5   # for journal access
6   group = "systemd-journal";
7   dataDir = "/var/lib/${user}";
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   + lib.optionalString (cfg.mailTo != null) ''
17     mail_to = ${cfg.mailTo}
18   ''
19   + cfg.extraConfig);
21   journalwatchPatterns = pkgs.writeText "patterns" ''
22     # (File Generated by NixOS journalwatch module.)
24     ${mkPatterns cfg.filterBlocks}
25   '';
27   # empty line at the end needed to to separate the blocks
28   mkPatterns = filterBlocks: lib.concatStringsSep "\n" (map (block: ''
29     ${block.match}
30     ${block.filters}
32   '') filterBlocks);
34   # can't use joinSymlinks directly, because when we point $XDG_CONFIG_HOME
35   # to the /nix/store path, we still need the subdirectory "journalwatch" inside that
36   # to match journalwatch's expectations
37   journalwatchConfigDir = pkgs.runCommand "journalwatch-config"
38     { preferLocalBuild = true; allowSubstitutes = false; }
39     ''
40       mkdir -p $out/journalwatch
41       ln -sf ${journalwatchConfig} $out/journalwatch/config
42       ln -sf ${journalwatchPatterns} $out/journalwatch/patterns
43     '';
46 in {
47   options = {
48     services.journalwatch = {
49       enable = lib.mkOption {
50         type = lib.types.bool;
51         default = false;
52         description = ''
53           If enabled, periodically check the journal with journalwatch and report the results by mail.
54         '';
55       };
57       package = lib.mkPackageOption pkgs "journalwatch" { };
59       priority = lib.mkOption {
60         type = lib.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       };
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 = lib.mkOption {
75         type = lib.types.str;
76         default = "journalwatch@${config.networking.hostName}";
77         defaultText = lib.literalExpression ''"journalwatch@''${config.networking.hostName}"'';
78         description = ''
79           Mail address to send journalwatch reports from.
80         '';
81       };
83       mailTo = lib.mkOption {
84         type = lib.types.nullOr lib.types.str;
85         default = null;
86         description = ''
87           Mail address to send journalwatch reports to.
88         '';
89       };
91       mailBinary = lib.mkOption {
92         type = lib.types.path;
93         default = "/run/wrappers/bin/sendmail";
94         description = ''
95           Sendmail-compatible binary to be used to send the messages.
96         '';
97       };
99       extraConfig = lib.mkOption {
100         type = lib.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       };
109       filterBlocks = lib.mkOption {
110         type = lib.types.listOf (lib.types.submodule {
111           options = {
112            match = lib.mkOption {
113               type = lib.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             };
126             filters = lib.mkOption {
127               type = lib.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         });
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           }
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         ];
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         ];
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.
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.
186           All regular expressions are extended Python regular expressions, for details
187           see: http://doc.pyschools.com/html/regex.html
188         '';
189       };
191       interval = lib.mkOption {
192         type = lib.types.str;
193         default = "hourly";
194         description = ''
195           How often to run journalwatch.
197           The format is described in systemd.time(7).
198         '';
199       };
200       accuracy = lib.mkOption {
201         type = lib.types.str;
202         default = "10min";
203         description = ''
204           The time window around the interval in which the journalwatch run will be scheduled.
206           The format is described in systemd.time(7).
207         '';
208       };
209     };
210   };
212   config = lib.mkIf cfg.enable {
214     users.users.${user} = {
215       isSystemUser = true;
216       home = dataDir;
217       group = group;
218     };
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     ];
226     systemd.services.journalwatch = {
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 = "${lib.getExe cfg.package} 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     };
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     };
260   };
262   meta = {
263     maintainers = with lib.maintainers; [ florianjacob ];
264   };