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";