diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 1f2fbb7d85c..7194e1f8385 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -243,6 +243,7 @@ ./services/logging/graylog.nix ./services/logging/heartbeat.nix ./services/logging/journalbeat.nix + ./services/logging/journalwatch.nix ./services/logging/klogd.nix ./services/logging/logcheck.nix ./services/logging/logrotate.nix diff --git a/nixos/modules/services/logging/journalwatch.nix b/nixos/modules/services/logging/journalwatch.nix new file mode 100644 index 00000000000..d49795fe2b7 --- /dev/null +++ b/nixos/modules/services/logging/journalwatch.nix @@ -0,0 +1,246 @@ +{ config, lib, pkgs, services, ... }: +with lib; + +let + cfg = config.services.journalwatch; + user = "journalwatch"; + dataDir = "/var/lib/${user}"; + + journalwatchConfig = pkgs.writeText "config" ('' + # (File Generated by NixOS journalwatch module.) + [DEFAULT] + mail_binary = ${cfg.mailBinary} + priority = ${toString cfg.priority} + mail_from = ${cfg.mailFrom} + '' + + optionalString (cfg.mailTo != null) '' + mail_to = ${cfg.mailTo} + '' + + cfg.extraConfig); + + journalwatchPatterns = pkgs.writeText "patterns" '' + # (File Generated by NixOS journalwatch module.) + + ${mkPatterns cfg.filterBlocks} + ''; + + # empty line at the end needed to to separate the blocks + mkPatterns = filterBlocks: concatStringsSep "\n" (map (block: '' + ${block.match} + ${block.filters} + + '') filterBlocks); + + +in { + options = { + services.journalwatch = { + enable = mkOption { + type = types.bool; + default = false; + description = '' + If enabled, periodically check the journal with journalwatch and report the results by mail. + ''; + }; + + priority = mkOption { + type = types.int; + default = 6; + description = '' + Lowest priority of message to be considered. + A value between 7 ("debug"), and 0 ("emerg"). Defaults to 6 ("info"). + If you don't care about anything with "info" priority, you can reduce + this to e.g. 5 ("notice") to considerably reduce the amount of + messages without needing many . + ''; + }; + + # HACK: this is a workaround for journalwatch's usage of socket.getfqdn() which always returns localhost if + # there's an alias for the localhost on a separate line in /etc/hosts, or take for ages if it's not present and + # then return something right-ish in the direction of /etc/hostname. Just bypass it completely. + mailFrom = mkOption { + type = types.str; + default = "journalwatch@${config.networking.hostName}"; + description = '' + Mail address to send journalwatch reports from. + ''; + }; + + mailTo = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Mail address to send journalwatch reports to. + ''; + }; + + mailBinary = mkOption { + type = types.path; + default = "/run/wrappers/bin/sendmail"; + description = '' + Sendmail-compatible binary to be used to send the messages. + ''; + }; + + extraConfig = mkOption { + type = types.str; + default = ""; + description = '' + Extra lines to be added verbatim to the journalwatch/config configuration file. + You can add any commandline argument to the config, without the '--'. + See journalwatch --help for all arguments and their description. + ''; + }; + + filterBlocks = mkOption { + type = types.listOf (types.submodule { + options = { + match = mkOption { + type = types.str; + example = "SYSLOG_IDENTIFIER = systemd"; + description = '' + Syntax: field = value + Specifies the log entry field this block should apply to. + If the field of a message matches this value, + this patternBlock's are applied. + If value starts and ends with a slash, it is interpreted as + an extended python regular expression, if not, it's an exact match. + The journal fields are explained in systemd.journal-fields(7). + ''; + }; + + filters = mkOption { + type = types.str; + example = '' + (Stopped|Stopping|Starting|Started) .* + (Reached target|Stopped target) .* + ''; + description = '' + The filters to apply on all messages which satisfy . + Any of those messages that match any specified filter will be removed from journalwatch's output. + Each filter is an extended Python regular expression. + You can specify multiple filters and separate them by newlines. + Lines starting with '#' are comments. Inline-comments are not permitted. + ''; + }; + }; + }); + + example = [ + # examples taken from upstream + { + match = "_SYSTEMD_UNIT = systemd-logind.service"; + filters = '' + New session [a-z]?\d+ of user \w+\. + Removed session [a-z]?\d+\. + ''; + } + + { + match = "SYSLOG_IDENTIFIER = /(CROND|crond)/"; + filters = '' + pam_unix\(crond:session\): session (opened|closed) for user \w+ + \(\w+\) CMD .* + ''; + } + ]; + + # another example from upstream. + # very useful on priority = 6, and required as journalwatch throws an error when no pattern is defined at all. + default = [ + { + match = "SYSLOG_IDENTIFIER = systemd"; + filters = '' + (Stopped|Stopping|Starting|Started) .* + (Created slice|Removed slice) user-\d*\.slice\. + Received SIGRTMIN\+24 from PID .* + (Reached target|Stopped target) .* + Startup finished in \d*ms\. + ''; + } + ]; + + + description = '' + filterBlocks can be defined to blacklist journal messages which are not errors. + Each block matches on a log entry field, and the filters in that block then are matched + against all messages with a matching log entry field. + + All messages whose PRIORITY is at least 6 (INFO) are processed by journalwatch. + If you don't specify any filterBlocks, PRIORITY is reduced to 5 (NOTICE) by default. + + All regular expressions are extended Python regular expressions, for details + see: http://doc.pyschools.com/html/regex.html + ''; + }; + + interval = mkOption { + type = types.str; + default = "hourly"; + description = '' + How often to run journalwatch. + + The format is described in systemd.time(7). + ''; + }; + accuracy = mkOption { + type = types.str; + default = "10min"; + description = '' + The time window around the interval in which the journalwatch run will be scheduled. + + The format is described in systemd.time(7). + ''; + }; + }; + }; + + config = mkIf cfg.enable { + + users.extraUsers.${user} = { + isSystemUser = true; + createHome = true; + home = dataDir; + # for journal access + group = "systemd-journal"; + }; + + systemd.services.journalwatch = { + environment = { + XDG_DATA_HOME = "${dataDir}/share"; + XDG_CONFIG_HOME = "${dataDir}/config"; + }; + serviceConfig = { + User = user; + Type = "oneshot"; + PermissionsStartOnly = true; + ExecStart = "${pkgs.python3Packages.journalwatch}/bin/journalwatch mail"; + # lowest CPU and IO priority, but both still in best-effort class to prevent starvation + Nice=19; + IOSchedulingPriority=7; + }; + preStart = '' + chown -R ${user}:systemd-journal ${dataDir} + chmod -R u+rwX,go-w ${dataDir} + mkdir -p ${dataDir}/config/journalwatch + ln -sf ${journalwatchConfig} ${dataDir}/config/journalwatch/config + ln -sf ${journalwatchPatterns} ${dataDir}/config/journalwatch/patterns + ''; + }; + + systemd.timers.journalwatch = { + description = "Periodic journalwatch run"; + wantedBy = [ "timers.target" ]; + timerConfig = { + OnCalendar = cfg.interval; + AccuracySec = cfg.accuracy; + Persistent = true; + }; + }; + + }; + + meta = { + maintainers = with stdenv.lib.maintainers; [ florianjacob ]; + }; +} diff --git a/pkgs/tools/system/journalwatch/default.nix b/pkgs/tools/system/journalwatch/default.nix new file mode 100644 index 00000000000..a424eb6c4b2 --- /dev/null +++ b/pkgs/tools/system/journalwatch/default.nix @@ -0,0 +1,43 @@ +{ stdenv, buildPythonPackage, fetchurl, fetchgit, pythonOlder, systemd, pytest }: + +buildPythonPackage rec { + pname = "journalwatch"; + name = "${pname}-${version}"; + version = "1.1.0"; + disabled = pythonOlder "3.3"; + + + src = fetchurl { + url = "https://github.com/The-Compiler/${pname}/archive/v${version}.tar.gz"; + sha512 = "3hvbgx95hjfivz9iv0hbhj720wvm32z86vj4a60lji2zdfpbqgr2b428lvg2cpvf71l2xn6ca5v0hzyz57qylgwqzgfrx7hqhl5g38s"; + }; + + # can be removed post 1.1.0 + postPatch = '' + substituteInPlace test_journalwatch.py \ + --replace "U Thu Jan 1 00:00:00 1970 prio foo [1337]" "U Thu Jan 1 00:00:00 1970 pprio foo [1337]" + ''; + + + doCheck = true; + + checkPhase = '' + pytest test_journalwatch.py + ''; + + buildInputs = [ + pytest + ]; + + propagatedBuildInputs = [ + systemd + ]; + + + meta = with stdenv.lib; { + description = "journalwatch is a tool to find error messages in the systemd journal."; + homepage = "https://github.com/The-Compiler/journalwatch"; + license = licenses.gpl3Plus; + maintainers = with maintainers; [ florianjacob ]; + }; +} diff --git a/pkgs/top-level/python-packages.nix b/pkgs/top-level/python-packages.nix index 4b6d1bf82e3..9694965c598 100644 --- a/pkgs/top-level/python-packages.nix +++ b/pkgs/top-level/python-packages.nix @@ -12382,6 +12382,9 @@ in { }; }; + journalwatch = callPackage ../tools/system/journalwatch { + inherit (self) systemd pytest; + }; jrnl = buildPythonPackage rec { name = "jrnl-1.9.7";