diff options
Diffstat (limited to 'nixos/modules/services/monitoring')
58 files changed, 2767 insertions, 275 deletions
diff --git a/nixos/modules/services/monitoring/alerta.nix b/nixos/modules/services/monitoring/alerta.nix index 34f2d41706a..7c6eff713cb 100644 --- a/nixos/modules/services/monitoring/alerta.nix +++ b/nixos/modules/services/monitoring/alerta.nix @@ -95,13 +95,13 @@ in ALERTA_SVR_CONF_FILE = alertaConf; }; serviceConfig = { - ExecStart = "${pkgs.python36Packages.alerta-server}/bin/alertad run --port ${toString cfg.port} --host ${cfg.bind}"; + ExecStart = "${pkgs.alerta-server}/bin/alertad run --port ${toString cfg.port} --host ${cfg.bind}"; User = "alerta"; Group = "alerta"; }; }; - environment.systemPackages = [ pkgs.python36Packages.alerta ]; + environment.systemPackages = [ pkgs.alerta ]; users.users.alerta = { uid = config.ids.uids.alerta; diff --git a/nixos/modules/services/monitoring/apcupsd.nix b/nixos/modules/services/monitoring/apcupsd.nix index 75218aa1d46..1dccbc93edf 100644 --- a/nixos/modules/services/monitoring/apcupsd.nix +++ b/nixos/modules/services/monitoring/apcupsd.nix @@ -104,7 +104,7 @@ in hooks = mkOption { default = {}; example = { - doshutdown = ''# shell commands to notify that the computer is shutting down''; + doshutdown = "# shell commands to notify that the computer is shutting down"; }; type = types.attrsOf types.lines; description = '' diff --git a/nixos/modules/services/monitoring/datadog-agent.nix b/nixos/modules/services/monitoring/datadog-agent.nix index f1cb890794e..b25a53435d0 100644 --- a/nixos/modules/services/monitoring/datadog-agent.nix +++ b/nixos/modules/services/monitoring/datadog-agent.nix @@ -6,7 +6,6 @@ let cfg = config.services.datadog-agent; ddConf = { - dd_url = "https://app.datadoghq.com"; skip_ssl_validation = false; confd_path = "/etc/datadog-agent/conf.d"; additional_checksd = "/etc/datadog-agent/checks.d"; @@ -14,6 +13,8 @@ let } // optionalAttrs (cfg.logLevel != null) { log_level = cfg.logLevel; } // optionalAttrs (cfg.hostname != null) { inherit (cfg) hostname; } + // optionalAttrs (cfg.ddUrl != null) { dd_url = cfg.ddUrl; } + // optionalAttrs (cfg.site != null) { site = cfg.site; } // optionalAttrs (cfg.tags != null ) { tags = concatStringsSep ", " cfg.tags; } // optionalAttrs (cfg.enableLiveProcessCollection) { process_config = { enabled = "true"; }; } // optionalAttrs (cfg.enableTraceAgent) { apm_config = { enabled = true; }; } @@ -77,6 +78,27 @@ in { type = types.path; }; + ddUrl = mkOption { + description = '' + Custom dd_url to configure the agent with. Useful if traffic to datadog + needs to go through a proxy. + Don't use this to point to another datadog site (EU) - use site instead. + ''; + default = null; + example = "http://haproxy.example.com:3834"; + type = types.nullOr types.str; + }; + + site = mkOption { + description = '' + The datadog site to point the agent towards. + Set to datadoghq.eu to point it to their EU site. + ''; + default = null; + example = "datadoghq.eu"; + type = types.nullOr types.str; + }; + tags = mkOption { description = "The tags to mark this Datadog agent"; example = [ "test" "service" ]; @@ -203,7 +225,7 @@ in { }; }; config = mkIf cfg.enable { - environment.systemPackages = [ datadogPkg pkgs.sysstat pkgs.procps pkgs.iproute ]; + environment.systemPackages = [ datadogPkg pkgs.sysstat pkgs.procps pkgs.iproute2 ]; users.users.datadog = { description = "Datadog Agent User"; @@ -217,7 +239,7 @@ in { systemd.services = let makeService = attrs: recursiveUpdate { - path = [ datadogPkg pkgs.python pkgs.sysstat pkgs.procps pkgs.iproute ]; + path = [ datadogPkg pkgs.python pkgs.sysstat pkgs.procps pkgs.iproute2 ]; wantedBy = [ "multi-user.target" ]; serviceConfig = { User = "datadog"; diff --git a/nixos/modules/services/monitoring/grafana-image-renderer.nix b/nixos/modules/services/monitoring/grafana-image-renderer.nix new file mode 100644 index 00000000000..b8b95d846c6 --- /dev/null +++ b/nixos/modules/services/monitoring/grafana-image-renderer.nix @@ -0,0 +1,150 @@ +{ lib, pkgs, config, ... }: + +with lib; + +let + cfg = config.services.grafana-image-renderer; + + format = pkgs.formats.json { }; + + configFile = format.generate "grafana-image-renderer-config.json" cfg.settings; +in { + options.services.grafana-image-renderer = { + enable = mkEnableOption "grafana-image-renderer"; + + chromium = mkOption { + type = types.package; + description = '' + The chromium to use for image rendering. + ''; + }; + + verbose = mkEnableOption "verbosity for the service"; + + provisionGrafana = mkEnableOption "Grafana configuration for grafana-image-renderer"; + + settings = mkOption { + type = types.submodule { + freeformType = format.type; + + options = { + service = { + port = mkOption { + type = types.port; + default = 8081; + description = '' + The TCP port to use for the rendering server. + ''; + }; + logging.level = mkOption { + type = types.enum [ "error" "warning" "info" "debug" ]; + default = "info"; + description = '' + The log-level of the <filename>grafana-image-renderer.service</filename>-unit. + ''; + }; + }; + rendering = { + width = mkOption { + default = 1000; + type = types.ints.positive; + description = '' + Width of the PNG used to display the alerting graph. + ''; + }; + height = mkOption { + default = 500; + type = types.ints.positive; + description = '' + Height of the PNG used to display the alerting graph. + ''; + }; + mode = mkOption { + default = "default"; + type = types.enum [ "default" "reusable" "clustered" ]; + description = '' + Rendering mode of <package>grafana-image-renderer</package>: + <itemizedlist> + <listitem><para><literal>default:</literal> Creates on browser-instance + per rendering request.</para></listitem> + <listitem><para><literal>reusable:</literal> One browser instance + will be started and reused for each rendering request.</para></listitem> + <listitem><para><literal>clustered:</literal> allows to precisely + configure how many browser-instances are supposed to be used. The values + for that mode can be declared in <literal>rendering.clustering</literal>. + </para></listitem> + </itemizedlist> + ''; + }; + args = mkOption { + type = types.listOf types.str; + default = [ "--no-sandbox" ]; + description = '' + List of CLI flags passed to <package>chromium</package>. + ''; + }; + }; + }; + }; + + default = {}; + + description = '' + Configuration attributes for <package>grafana-image-renderer</package>. + + See <link xlink:href="https://github.com/grafana/grafana-image-renderer/blob/ce1f81438e5f69c7fd7c73ce08bab624c4c92e25/default.json" /> + for supported values. + ''; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + { assertion = cfg.provisionGrafana -> config.services.grafana.enable; + message = '' + To provision a Grafana instance to use grafana-image-renderer, + `services.grafana.enable` must be set to `true`! + ''; + } + ]; + + services.grafana.extraOptions = mkIf cfg.provisionGrafana { + RENDERING_SERVER_URL = "http://localhost:${toString cfg.settings.service.port}/render"; + RENDERING_CALLBACK_URL = "http://localhost:${toString config.services.grafana.port}"; + }; + + services.grafana-image-renderer.chromium = mkDefault pkgs.chromium; + + services.grafana-image-renderer.settings = { + rendering = mapAttrs (const mkDefault) { + chromeBin = "${cfg.chromium}/bin/chromium"; + verboseLogging = cfg.verbose; + timezone = config.time.timeZone; + }; + + service = { + logging.level = mkIf cfg.verbose (mkDefault "debug"); + metrics.enabled = mkDefault false; + }; + }; + + systemd.services.grafana-image-renderer = { + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + description = " A Grafana backend plugin that handles rendering of panels & dashboards to PNGs using headless browser (Chromium/Chrome)"; + + environment = { + PUPPETEER_SKIP_CHROMIUM_DOWNLOAD = "true"; + }; + + serviceConfig = { + DynamicUser = true; + PrivateTmp = true; + ExecStart = "${pkgs.grafana-image-renderer}/bin/grafana-image-renderer server --config=${configFile}"; + Restart = "always"; + }; + }; + }; + + meta.maintainers = with maintainers; [ ma27 ]; +} diff --git a/nixos/modules/services/monitoring/grafana.nix b/nixos/modules/services/monitoring/grafana.nix index b0c81a46d4d..e0b2624b6ca 100644 --- a/nixos/modules/services/monitoring/grafana.nix +++ b/nixos/modules/services/monitoring/grafana.nix @@ -5,15 +5,17 @@ with lib; let cfg = config.services.grafana; opt = options.services.grafana; + declarativePlugins = pkgs.linkFarm "grafana-plugins" (builtins.map (pkg: { name = pkg.pname; path = pkg; }) cfg.declarativePlugins); envOptions = { PATHS_DATA = cfg.dataDir; - PATHS_PLUGINS = "${cfg.dataDir}/plugins"; + PATHS_PLUGINS = if builtins.isNull cfg.declarativePlugins then "${cfg.dataDir}/plugins" else declarativePlugins; PATHS_LOGS = "${cfg.dataDir}/log"; SERVER_PROTOCOL = cfg.protocol; SERVER_HTTP_ADDR = cfg.addr; SERVER_HTTP_PORT = cfg.port; + SERVER_SOCKET = cfg.socket; SERVER_DOMAIN = cfg.domain; SERVER_ROOT_URL = cfg.rootUrl; SERVER_STATIC_ROOT_PATH = cfg.staticRootPath; @@ -40,6 +42,9 @@ let AUTH_ANONYMOUS_ENABLED = boolToString cfg.auth.anonymous.enable; AUTH_ANONYMOUS_ORG_NAME = cfg.auth.anonymous.org_name; AUTH_ANONYMOUS_ORG_ROLE = cfg.auth.anonymous.org_role; + AUTH_GOOGLE_ENABLED = boolToString cfg.auth.google.enable; + AUTH_GOOGLE_ALLOW_SIGN_UP = boolToString cfg.auth.google.allowSignUp; + AUTH_GOOGLE_CLIENT_ID = cfg.auth.google.clientId; ANALYTICS_REPORTING_ENABLED = boolToString cfg.analytics.reporting.enable; @@ -64,10 +69,18 @@ let dashboardFile = pkgs.writeText "dashboard.yaml" (builtins.toJSON dashboardConfiguration); + notifierConfiguration = { + apiVersion = 1; + notifiers = cfg.provision.notifiers; + }; + + notifierFile = pkgs.writeText "notifier.yaml" (builtins.toJSON notifierConfiguration); + provisionConfDir = pkgs.runCommand "grafana-provisioning" { } '' - mkdir -p $out/{datasources,dashboards} + mkdir -p $out/{datasources,dashboards,notifiers} ln -sf ${datasourceFile} $out/datasources/datasource.yaml ln -sf ${dashboardFile} $out/dashboards/dashboard.yaml + ln -sf ${notifierFile} $out/notifiers/notifier.yaml ''; # Get a submodule without any embedded metadata: @@ -78,80 +91,80 @@ let options = { name = mkOption { type = types.str; - description = "Name of the datasource. Required"; + description = "Name of the datasource. Required."; }; type = mkOption { - type = types.enum ["graphite" "prometheus" "cloudwatch" "elasticsearch" "influxdb" "opentsdb" "mysql" "mssql" "postgres" "loki"]; - description = "Datasource type. Required"; + type = types.str; + description = "Datasource type. Required."; }; access = mkOption { type = types.enum ["proxy" "direct"]; default = "proxy"; - description = "Access mode. proxy or direct (Server or Browser in the UI). Required"; + description = "Access mode. proxy or direct (Server or Browser in the UI). Required."; }; orgId = mkOption { type = types.int; default = 1; - description = "Org id. will default to orgId 1 if not specified"; + description = "Org id. will default to orgId 1 if not specified."; }; url = mkOption { type = types.str; - description = "Url of the datasource"; + description = "Url of the datasource."; }; password = mkOption { type = types.nullOr types.str; default = null; - description = "Database password, if used"; + description = "Database password, if used."; }; user = mkOption { type = types.nullOr types.str; default = null; - description = "Database user, if used"; + description = "Database user, if used."; }; database = mkOption { type = types.nullOr types.str; default = null; - description = "Database name, if used"; + description = "Database name, if used."; }; basicAuth = mkOption { type = types.nullOr types.bool; default = null; - description = "Enable/disable basic auth"; + description = "Enable/disable basic auth."; }; basicAuthUser = mkOption { type = types.nullOr types.str; default = null; - description = "Basic auth username"; + description = "Basic auth username."; }; basicAuthPassword = mkOption { type = types.nullOr types.str; default = null; - description = "Basic auth password"; + description = "Basic auth password."; }; withCredentials = mkOption { type = types.bool; default = false; - description = "Enable/disable with credentials headers"; + description = "Enable/disable with credentials headers."; }; isDefault = mkOption { type = types.bool; default = false; - description = "Mark as default datasource. Max one per org"; + description = "Mark as default datasource. Max one per org."; }; jsonData = mkOption { type = types.nullOr types.attrs; default = null; - description = "Datasource specific configuration"; + description = "Datasource specific configuration."; }; secureJsonData = mkOption { type = types.nullOr types.attrs; default = null; - description = "Datasource specific secure configuration"; + description = "Datasource specific secure configuration."; }; version = mkOption { type = types.int; default = 1; - description = "Version"; + description = "Version."; }; editable = mkOption { type = types.bool; @@ -167,41 +180,99 @@ let name = mkOption { type = types.str; default = "default"; - description = "Provider name"; + description = "Provider name."; }; orgId = mkOption { type = types.int; default = 1; - description = "Organization ID"; + description = "Organization ID."; }; folder = mkOption { type = types.str; default = ""; - description = "Add dashboards to the specified folder"; + description = "Add dashboards to the specified folder."; }; type = mkOption { type = types.str; default = "file"; - description = "Dashboard provider type"; + description = "Dashboard provider type."; }; disableDeletion = mkOption { type = types.bool; default = false; - description = "Disable deletion when JSON file is removed"; + description = "Disable deletion when JSON file is removed."; }; updateIntervalSeconds = mkOption { type = types.int; default = 10; - description = "How often Grafana will scan for changed dashboards"; + description = "How often Grafana will scan for changed dashboards."; }; options = { path = mkOption { type = types.path; - description = "Path grafana will watch for dashboards"; + description = "Path grafana will watch for dashboards."; }; }; }; }; + + grafanaTypes.notifierConfig = types.submodule { + options = { + name = mkOption { + type = types.str; + default = "default"; + description = "Notifier name."; + }; + type = mkOption { + type = types.enum ["dingding" "discord" "email" "googlechat" "hipchat" "kafka" "line" "teams" "opsgenie" "pagerduty" "prometheus-alertmanager" "pushover" "sensu" "sensugo" "slack" "telegram" "threema" "victorops" "webhook"]; + description = "Notifier type."; + }; + uid = mkOption { + type = types.str; + description = "Unique notifier identifier."; + }; + org_id = mkOption { + type = types.int; + default = 1; + description = "Organization ID."; + }; + org_name = mkOption { + type = types.str; + default = "Main Org."; + description = "Organization name."; + }; + is_default = mkOption { + type = types.bool; + description = "Is the default notifier."; + default = false; + }; + send_reminder = mkOption { + type = types.bool; + default = true; + description = "Should the notifier be sent reminder notifications while alerts continue to fire."; + }; + frequency = mkOption { + type = types.str; + default = "5m"; + description = "How frequently should the notifier be sent reminders."; + }; + disable_resolve_message = mkOption { + type = types.bool; + default = false; + description = "Turn off the message that sends when an alert returns to OK."; + }; + settings = mkOption { + type = types.nullOr types.attrs; + default = null; + description = "Settings for the notifier type."; + }; + secure_settings = mkOption { + type = types.nullOr types.attrs; + default = null; + description = "Secure settings for the notifier type."; + }; + }; + }; in { options.services.grafana = { enable = mkEnableOption "grafana"; @@ -221,7 +292,13 @@ in { port = mkOption { description = "Listening port."; default = 3000; - type = types.int; + type = types.port; + }; + + socket = mkOption { + description = "Listening socket."; + default = "/run/grafana/grafana.sock"; + type = types.str; }; domain = mkOption { @@ -261,6 +338,17 @@ in { type = types.package; }; + declarativePlugins = mkOption { + type = with types; nullOr (listOf path); + default = null; + description = "If non-null, then a list of packages containing Grafana plugins to install. If set, plugins cannot be manually installed."; + example = literalExample "with pkgs.grafanaPlugins; [ grafana-piechart-panel ]"; + # Make sure each plugin is added only once; otherwise building + # the link farm fails, since the same path is added multiple + # times. + apply = x: if isList x then lib.unique x else x; + }; + dataDir = mkOption { description = "Data directory."; default = "/var/lib/grafana"; @@ -330,17 +418,23 @@ in { provision = { enable = mkEnableOption "provision"; datasources = mkOption { - description = "Grafana datasources configuration"; + description = "Grafana datasources configuration."; default = []; type = types.listOf grafanaTypes.datasourceConfig; apply = x: map _filter x; }; dashboards = mkOption { - description = "Grafana dashboard configuration"; + description = "Grafana dashboard configuration."; default = []; type = types.listOf grafanaTypes.dashboardConfig; apply = x: map _filter x; }; + notifiers = mkOption { + description = "Grafana notifier configuration."; + default = []; + type = types.listOf grafanaTypes.notifierConfig; + apply = x: map _filter x; + }; }; security = { @@ -384,12 +478,12 @@ in { smtp = { enable = mkEnableOption "smtp"; host = mkOption { - description = "Host to connect to"; + description = "Host to connect to."; default = "localhost:25"; type = types.str; }; user = mkOption { - description = "User used for authentication"; + description = "User used for authentication."; default = ""; type = types.str; }; @@ -410,7 +504,7 @@ in { type = types.nullOr types.path; }; fromAddress = mkOption { - description = "Email address used for sending"; + description = "Email address used for sending."; default = "admin@grafana.localhost"; type = types.str; }; @@ -418,7 +512,7 @@ in { users = { allowSignUp = mkOption { - description = "Disable user signup / registration"; + description = "Disable user signup / registration."; default = false; type = types.bool; }; @@ -442,28 +536,51 @@ in { }; }; - auth.anonymous = { - enable = mkOption { - description = "Whether to allow anonymous access"; - default = false; - type = types.bool; - }; - org_name = mkOption { - description = "Which organization to allow anonymous access to"; - default = "Main Org."; - type = types.str; + auth = { + anonymous = { + enable = mkOption { + description = "Whether to allow anonymous access."; + default = false; + type = types.bool; + }; + org_name = mkOption { + description = "Which organization to allow anonymous access to."; + default = "Main Org."; + type = types.str; + }; + org_role = mkOption { + description = "Which role anonymous users have in the organization."; + default = "Viewer"; + type = types.str; + }; }; - org_role = mkOption { - description = "Which role anonymous users have in the organization"; - default = "Viewer"; - type = types.str; + google = { + enable = mkOption { + description = "Whether to allow Google OAuth2."; + default = false; + type = types.bool; + }; + allowSignUp = mkOption { + description = "Whether to allow sign up with Google OAuth2."; + default = false; + type = types.bool; + }; + clientId = mkOption { + description = "Google OAuth2 client ID."; + default = ""; + type = types.str; + }; + clientSecretFile = mkOption { + description = "Google OAuth2 client secret."; + default = null; + type = types.nullOr types.path; + }; }; - }; analytics.reporting = { enable = mkOption { - description = "Whether to allow anonymous usage reporting to stats.grafana.net"; + description = "Whether to allow anonymous usage reporting to stats.grafana.net."; default = true; type = types.bool; }; @@ -489,6 +606,9 @@ in { (optional ( any (x: x.password != null || x.basicAuthPassword != null || x.secureJsonData != null) cfg.provision.datasources ) "Datasource passwords will be stored as plaintext in the Nix store!") + (optional ( + any (x: x.secure_settings != null) cfg.provision.notifiers + ) "Notifier secure settings will be stored as plaintext in the Nix store!") ]; environment.systemPackages = [ cfg.package ]; @@ -520,17 +640,28 @@ in { QT_QPA_PLATFORM = "offscreen"; } // mapAttrs' (n: v: nameValuePair "GF_${n}" (toString v)) envOptions; script = '' + set -o errexit -o pipefail -o nounset -o errtrace + shopt -s inherit_errexit + + ${optionalString (cfg.auth.google.clientSecretFile != null) '' + GF_AUTH_GOOGLE_CLIENT_SECRET="$(<${escapeShellArg cfg.auth.google.clientSecretFile})" + export GF_AUTH_GOOGLE_CLIENT_SECRET + ''} ${optionalString (cfg.database.passwordFile != null) '' - export GF_DATABASE_PASSWORD="$(cat ${escapeShellArg cfg.database.passwordFile})" + GF_DATABASE_PASSWORD="$(<${escapeShellArg cfg.database.passwordFile})" + export GF_DATABASE_PASSWORD ''} ${optionalString (cfg.security.adminPasswordFile != null) '' - export GF_SECURITY_ADMIN_PASSWORD="$(cat ${escapeShellArg cfg.security.adminPasswordFile})" + GF_SECURITY_ADMIN_PASSWORD="$(<${escapeShellArg cfg.security.adminPasswordFile})" + export GF_SECURITY_ADMIN_PASSWORD ''} ${optionalString (cfg.security.secretKeyFile != null) '' - export GF_SECURITY_SECRET_KEY="$(cat ${escapeShellArg cfg.security.secretKeyFile})" + GF_SECURITY_SECRET_KEY="$(<${escapeShellArg cfg.security.secretKeyFile})" + export GF_SECURITY_SECRET_KEY ''} ${optionalString (cfg.smtp.passwordFile != null) '' - export GF_SMTP_PASSWORD="$(cat ${escapeShellArg cfg.smtp.passwordFile})" + GF_SMTP_PASSWORD="$(<${escapeShellArg cfg.smtp.passwordFile})" + export GF_SMTP_PASSWORD ''} ${optionalString cfg.provision.enable '' export GF_PATHS_PROVISIONING=${provisionConfDir}; @@ -540,6 +671,8 @@ in { serviceConfig = { WorkingDirectory = cfg.dataDir; User = "grafana"; + RuntimeDirectory = "grafana"; + RuntimeDirectoryMode = "0755"; }; preStart = '' ln -fs ${cfg.package}/share/grafana/conf ${cfg.dataDir} diff --git a/nixos/modules/services/monitoring/graphite.nix b/nixos/modules/services/monitoring/graphite.nix index 64d9d61950d..9213748d3c9 100644 --- a/nixos/modules/services/monitoring/graphite.nix +++ b/nixos/modules/services/monitoring/graphite.nix @@ -25,10 +25,10 @@ let graphiteApiConfig = pkgs.writeText "graphite-api.yaml" '' search_index: ${dataDir}/index - ${optionalString (config.time.timeZone != null) ''time_zone: ${config.time.timeZone}''} - ${optionalString (cfg.api.finders != []) ''finders:''} + ${optionalString (config.time.timeZone != null) "time_zone: ${config.time.timeZone}"} + ${optionalString (cfg.api.finders != []) "finders:"} ${concatMapStringsSep "\n" (f: " - " + f.moduleName) cfg.api.finders} - ${optionalString (cfg.api.functions != []) ''functions:''} + ${optionalString (cfg.api.functions != []) "functions:"} ${concatMapStringsSep "\n" (f: " - " + f) cfg.api.functions} ${cfg.api.extraConfig} ''; diff --git a/nixos/modules/services/monitoring/incron.nix b/nixos/modules/services/monitoring/incron.nix index 1789fd9f205..dc97af58562 100644 --- a/nixos/modules/services/monitoring/incron.nix +++ b/nixos/modules/services/monitoring/incron.nix @@ -67,7 +67,7 @@ in config = mkIf cfg.enable { warnings = optional (cfg.allow != null && cfg.deny != null) - ''If `services.incron.allow` is set then `services.incron.deny` will be ignored.''; + "If `services.incron.allow` is set then `services.incron.deny` will be ignored."; environment.systemPackages = [ pkgs.incron ]; diff --git a/nixos/modules/services/monitoring/loki.nix b/nixos/modules/services/monitoring/loki.nix index f4eec7e0d28..51cabaa274a 100644 --- a/nixos/modules/services/monitoring/loki.nix +++ b/nixos/modules/services/monitoring/loki.nix @@ -39,7 +39,7 @@ in { }; configuration = mkOption { - type = types.attrs; + type = (pkgs.formats.json {}).type; default = {}; description = '' Specify the configuration for Loki in Nix. @@ -78,6 +78,8 @@ in { ''; }]; + environment.systemPackages = [ pkgs.grafana-loki ]; # logcli + users.groups.${cfg.group} = { }; users.users.${cfg.user} = { description = "Loki Service User"; diff --git a/nixos/modules/services/monitoring/mackerel-agent.nix b/nixos/modules/services/monitoring/mackerel-agent.nix new file mode 100644 index 00000000000..7046de9d403 --- /dev/null +++ b/nixos/modules/services/monitoring/mackerel-agent.nix @@ -0,0 +1,111 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.mackerel-agent; + settingsFmt = pkgs.formats.toml {}; +in { + options.services.mackerel-agent = { + enable = mkEnableOption "mackerel.io agent"; + + # the upstream package runs as root, but doesn't seem to be strictly + # necessary for basic functionality + runAsRoot = mkEnableOption "Whether to run as root."; + + autoRetirement = mkEnableOption '' + Whether to automatically retire the host upon OS shutdown. + ''; + + apiKeyFile = mkOption { + type = types.path; + default = ""; + example = "/run/keys/mackerel-api-key"; + description = '' + Path to file containing the Mackerel API key. The file should contain a + single line of the following form: + + <literallayout>apikey = "EXAMPLE_API_KEY"</literallayout> + ''; + }; + + settings = mkOption { + description = '' + Options for mackerel-agent.conf. + + Documentation: + <link xlink:href="https://mackerel.io/docs/entry/spec/agent"/> + ''; + + default = {}; + example = { + verbose = false; + silent = false; + }; + + type = types.submodule { + freeformType = settingsFmt.type; + + options.host_status = { + on_start = mkOption { + type = types.enum [ "working" "standby" "maintenance" "poweroff" ]; + description = "Host status after agent startup."; + default = "working"; + }; + on_stop = mkOption { + type = types.enum [ "working" "standby" "maintenance" "poweroff" ]; + description = "Host status after agent shutdown."; + default = "poweroff"; + }; + }; + + options.diagnostic = + mkEnableOption "Collect memory usage for the agent itself"; + }; + }; + }; + + config = mkIf cfg.enable { + environment.systemPackages = with pkgs; [ mackerel-agent ]; + + environment.etc = { + "mackerel-agent/mackerel-agent.conf".source = + settingsFmt.generate "mackerel-agent.conf" cfg.settings; + "mackerel-agent/conf.d/api-key.conf".source = cfg.apiKeyFile; + }; + + services.mackerel-agent.settings = { + root = mkDefault "/var/lib/mackerel-agent"; + pidfile = mkDefault "/run/mackerel-agent/mackerel-agent.pid"; + + # conf.d stores the symlink to cfg.apiKeyFile + include = mkDefault "/etc/mackerel-agent/conf.d/*.conf"; + }; + + # upstream service file in https://git.io/JUt4Q + systemd.services.mackerel-agent = { + description = "mackerel.io agent"; + after = [ "network-online.target" "nss-lookup.target" ]; + wantedBy = [ "multi-user.target" ]; + environment = { + MACKEREL_PLUGIN_WORKDIR = mkDefault "%C/mackerel-agent"; + }; + serviceConfig = { + DynamicUser = !cfg.runAsRoot; + PrivateTmp = mkDefault true; + CacheDirectory = "mackerel-agent"; + ConfigurationDirectory = "mackerel-agent"; + RuntimeDirectory = "mackerel-agent"; + StateDirectory = "mackerel-agent"; + ExecStart = "${pkgs.mackerel-agent}/bin/mackerel-agent supervise"; + ExecStopPost = mkIf cfg.autoRetirement "${pkg.mackerel-agent}/bin/mackerel-agent retire -force"; + ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; + LimitNOFILE = mkDefault 65536; + LimitNPROC = mkDefault 65536; + }; + restartTriggers = [ + config.environment.etc."mackerel-agent/mackerel-agent.conf".source + ]; + }; + }; +} diff --git a/nixos/modules/services/monitoring/metricbeat.nix b/nixos/modules/services/monitoring/metricbeat.nix new file mode 100644 index 00000000000..b285559eaa9 --- /dev/null +++ b/nixos/modules/services/monitoring/metricbeat.nix @@ -0,0 +1,152 @@ +{ config, lib, pkgs, ... }: + +let + inherit (lib) + attrValues + literalExample + mkEnableOption + mkIf + mkOption + types + ; + cfg = config.services.metricbeat; + + settingsFormat = pkgs.formats.yaml {}; + +in +{ + options = { + + services.metricbeat = { + + enable = mkEnableOption "metricbeat"; + + package = mkOption { + type = types.package; + default = pkgs.metricbeat; + defaultText = literalExample "pkgs.metricbeat"; + example = literalExample "pkgs.metricbeat7"; + description = '' + The metricbeat package to use + ''; + }; + + modules = mkOption { + description = '' + Metricbeat modules are responsible for reading metrics from the various sources. + + This is like <literal>services.metricbeat.settings.metricbeat.modules</literal>, + but structured as an attribute set. This has the benefit that multiple + NixOS modules can contribute settings to a single metricbeat module. + + A module can be specified multiple times by choosing a different <literal><name></literal> + for each, but setting <xref linkend="opt-services.metricbeat.modules._name_.module"/> to the same value. + + See <link xlink:href="https://www.elastic.co/guide/en/beats/metricbeat/current/metricbeat-modules.html"/>. + ''; + default = {}; + type = types.attrsOf (types.submodule ({ name, ... }: { + freeformType = settingsFormat.type; + options = { + module = mkOption { + type = types.str; + default = name; + defaultText = literalExample ''<name>''; + description = '' + The name of the module. + + Look for the value after <literal>module:</literal> on the individual + module pages linked from <link xlink:href="https://www.elastic.co/guide/en/beats/metricbeat/current/metricbeat-modules.html"/>. + ''; + }; + }; + })); + example = { + system = { + metricsets = ["cpu" "load" "memory" "network" "process" "process_summary" "uptime" "socket_summary"]; + enabled = true; + period = "10s"; + processes = [".*"]; + cpu.metrics = ["percentages" "normalized_percentages"]; + core.metrics = ["percentages"]; + }; + }; + }; + + settings = mkOption { + type = types.submodule { + freeformType = settingsFormat.type; + options = { + + name = mkOption { + type = types.str; + default = ""; + description = '' + Name of the beat. Defaults to the hostname. + See <link xlink:href="https://www.elastic.co/guide/en/beats/metricbeat/current/configuration-general-options.html#_name"/>. + ''; + }; + + tags = mkOption { + type = types.listOf types.str; + default = []; + description = '' + Tags to place on the shipped metrics. + See <link xlink:href="https://www.elastic.co/guide/en/beats/metricbeat/current/configuration-general-options.html#_tags_2"/>. + ''; + }; + + metricbeat.modules = mkOption { + type = types.listOf settingsFormat.type; + default = []; + internal = true; + description = '' + The metric collecting modules. Use <xref linkend="opt-services.metricbeat.modules"/> instead. + + See <link xlink:href="https://www.elastic.co/guide/en/beats/metricbeat/current/metricbeat-modules.html"/>. + ''; + }; + }; + }; + default = {}; + description = '' + Configuration for metricbeat. See <link xlink:href="https://www.elastic.co/guide/en/beats/metricbeat/current/configuring-howto-metricbeat.html"/> for supported values. + ''; + }; + + }; + }; + + config = mkIf cfg.enable { + + assertions = [ + { + # empty modules would cause a failure at runtime + assertion = cfg.settings.metricbeat.modules != []; + message = "services.metricbeat: You must configure one or more modules."; + } + ]; + + services.metricbeat.settings.metricbeat.modules = attrValues cfg.modules; + + systemd.services.metricbeat = { + description = "metricbeat metrics shipper"; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + ExecStart = '' + ${cfg.package}/bin/metricbeat \ + -c ${settingsFormat.generate "metricbeat.yml" cfg.settings} \ + --path.data $STATE_DIRECTORY \ + --path.logs $LOGS_DIRECTORY \ + ; + ''; + Restart = "always"; + DynamicUser = true; + ProtectSystem = "strict"; + ProtectHome = "tmpfs"; + StateDirectory = "metricbeat"; + LogsDirectory = "metricbeat"; + }; + }; + }; +} diff --git a/nixos/modules/services/monitoring/monit.nix b/nixos/modules/services/monitoring/monit.nix index aa51b83912c..379ee967620 100644 --- a/nixos/modules/services/monitoring/monit.nix +++ b/nixos/modules/services/monitoring/monit.nix @@ -4,29 +4,19 @@ with lib; let cfg = config.services.monit; - extraConfig = pkgs.writeText "monitConfig" cfg.extraConfig; in { - imports = [ - (mkRenamedOptionModule [ "services" "monit" "config" ] ["services" "monit" "extraConfig" ]) - ]; - options.services.monit = { enable = mkEnableOption "Monit"; - configFiles = mkOption { - type = types.listOf types.path; - default = []; - description = "List of paths to be included in the monitrc file"; - }; - - extraConfig = mkOption { + config = mkOption { type = types.lines; default = ""; - description = "Additional monit config as string"; + description = "monitrc content"; }; + }; config = mkIf cfg.enable { @@ -34,7 +24,7 @@ in environment.systemPackages = [ pkgs.monit ]; environment.etc.monitrc = { - text = concatMapStringsSep "\n" (path: "include ${path}") (cfg.configFiles ++ [extraConfig]); + text = cfg.config; mode = "0400"; }; @@ -53,4 +43,6 @@ in }; }; + + meta.maintainers = with maintainers; [ ryantm ]; } diff --git a/nixos/modules/services/monitoring/nagios.nix b/nixos/modules/services/monitoring/nagios.nix index 9ac6869068f..61214508a9c 100644 --- a/nixos/modules/services/monitoring/nagios.nix +++ b/nixos/modules/services/monitoring/nagios.nix @@ -192,6 +192,7 @@ in path = [ pkgs.nagios ] ++ cfg.plugins; wantedBy = [ "multi-user.target" ]; after = [ "network.target" ]; + restartTriggers = [ nagiosCfgFile ]; serviceConfig = { User = "nagios"; @@ -201,7 +202,6 @@ in LogsDirectory = "nagios"; StateDirectory = "nagios"; ExecStart = "${pkgs.nagios}/bin/nagios /etc/nagios.cfg"; - X-ReloadIfChanged = nagiosCfgFile; }; }; diff --git a/nixos/modules/services/monitoring/netdata.nix b/nixos/modules/services/monitoring/netdata.nix index 2e73e15d3a8..561ce3eec62 100644 --- a/nixos/modules/services/monitoring/netdata.nix +++ b/nixos/modules/services/monitoring/netdata.nix @@ -8,6 +8,7 @@ let wrappedPlugins = pkgs.runCommand "wrapped-plugins" { preferLocalBuild = true; } '' mkdir -p $out/libexec/netdata/plugins.d ln -s /run/wrappers/bin/apps.plugin $out/libexec/netdata/plugins.d/apps.plugin + ln -s /run/wrappers/bin/cgroup-network $out/libexec/netdata/plugins.d/cgroup-network ln -s /run/wrappers/bin/freeipmi.plugin $out/libexec/netdata/plugins.d/freeipmi.plugin ln -s /run/wrappers/bin/perf.plugin $out/libexec/netdata/plugins.d/perf.plugin ln -s /run/wrappers/bin/slabinfo.plugin $out/libexec/netdata/plugins.d/slabinfo.plugin @@ -26,6 +27,10 @@ let "web files owner" = "root"; "web files group" = "root"; }; + "plugin:cgroups" = { + "script to get cgroup network interfaces" = "${wrappedPlugins}/libexec/netdata/plugins.d/cgroup-network"; + "use unified cgroups" = "yes"; + }; }; mkConfig = generators.toINI {} (recursiveUpdate localConfig cfg.config); configFile = pkgs.writeText "netdata.conf" (if cfg.configText != null then cfg.configText else mkConfig); @@ -77,6 +82,7 @@ in { ''; }; extraPackages = mkOption { + type = types.functionTo (types.listOf types.package); default = ps: []; defaultText = "ps: []"; example = literalExample '' @@ -122,9 +128,20 @@ in { "error log" = "syslog"; }; ''; - }; + }; + + enableAnalyticsReporting = mkOption { + type = types.bool; + default = false; + description = '' + Enable reporting of anonymous usage statistics to Netdata Inc. via either + Google Analytics (in versions prior to 1.29.4), or Netdata Inc.'s + self-hosted PostHog (in versions 1.29.4 and later). + See: <link xlink:href="https://learn.netdata.cloud/docs/agent/anonymous-statistics"/> + ''; }; }; + }; config = mkIf cfg.enable { assertions = @@ -137,12 +154,17 @@ in { description = "Real time performance monitoring"; after = [ "network.target" ]; wantedBy = [ "multi-user.target" ]; - path = (with pkgs; [ curl gawk which ]) ++ lib.optional cfg.python.enable - (pkgs.python3.withPackages cfg.python.extraPackages); + path = (with pkgs; [ curl gawk iproute2 which ]) + ++ lib.optional cfg.python.enable (pkgs.python3.withPackages cfg.python.extraPackages) + ++ lib.optional config.virtualisation.libvirtd.enable (config.virtualisation.libvirtd.package); + environment = { + PYTHONPATH = "${cfg.package}/libexec/netdata/python.d/python_modules"; + } // lib.optionalAttrs (!cfg.enableAnalyticsReporting) { + DO_NOT_TRACK = "1"; + }; serviceConfig = { - Environment="PYTHONPATH=${cfg.package}/libexec/netdata/python.d/python_modules"; ExecStart = "${cfg.package}/bin/netdata -P /run/netdata/netdata.pid -D -c ${configFile}"; - ExecReload = "${pkgs.utillinux}/bin/kill -s HUP -s USR1 -s USR2 $MAINPID"; + ExecReload = "${pkgs.util-linux}/bin/kill -s HUP -s USR1 -s USR2 $MAINPID"; TimeoutStopSec = 60; Restart = "on-failure"; # User and group @@ -175,6 +197,8 @@ in { "CAP_SYS_PTRACE" # is required for apps plugin "CAP_SYS_RESOURCE" # is required for ebpf plugin "CAP_NET_RAW" # is required for fping app + "CAP_SYS_CHROOT" # is required for cgroups plugin + "CAP_SETUID" # is required for cgroups and cgroups-network plugins ]; # Sandboxing ProtectSystem = "full"; @@ -192,7 +216,15 @@ in { capabilities = "cap_dac_read_search,cap_sys_ptrace+ep"; owner = cfg.user; group = cfg.group; - permissions = "u+rx,g+rx,o-rwx"; + permissions = "u+rx,g+x,o-rwx"; + }; + + security.wrappers."cgroup-network" = { + source = "${cfg.package}/libexec/netdata/plugins.d/cgroup-network.org"; + capabilities = "cap_setuid+ep"; + owner = cfg.user; + group = cfg.group; + permissions = "u+rx,g+x,o-rwx"; }; security.wrappers."freeipmi.plugin" = { @@ -200,7 +232,7 @@ in { capabilities = "cap_dac_override,cap_fowner+ep"; owner = cfg.user; group = cfg.group; - permissions = "u+rx,g+rx,o-rwx"; + permissions = "u+rx,g+x,o-rwx"; }; security.wrappers."perf.plugin" = { @@ -208,7 +240,7 @@ in { capabilities = "cap_sys_admin+ep"; owner = cfg.user; group = cfg.group; - permissions = "u+rx,g+rx,o-rx"; + permissions = "u+rx,g+x,o-rwx"; }; security.wrappers."slabinfo.plugin" = { @@ -216,7 +248,7 @@ in { capabilities = "cap_dac_override+ep"; owner = cfg.user; group = cfg.group; - permissions = "u+rx,g+rx,o-rx"; + permissions = "u+rx,g+x,o-rwx"; }; security.pam.loginLimits = [ diff --git a/nixos/modules/services/monitoring/prometheus/default.nix b/nixos/modules/services/monitoring/prometheus/default.nix index d7e06484b69..3be247ffb24 100644 --- a/nixos/modules/services/monitoring/prometheus/default.nix +++ b/nixos/modules/services/monitoring/prometheus/default.nix @@ -32,6 +32,8 @@ let (pkgs.writeText "prometheus.rules" (concatStringsSep "\n" cfg.rules)) ]); scrape_configs = filterValidPrometheus cfg.scrapeConfigs; + remote_write = filterValidPrometheus cfg.remoteWrite; + remote_read = filterValidPrometheus cfg.remoteRead; alerting = { inherit (cfg) alertmanagers; }; @@ -45,12 +47,12 @@ let cmdlineArgs = cfg.extraFlags ++ [ "--storage.tsdb.path=${workingDir}/data/" - "--config.file=${prometheusYml}" + "--config.file=/run/prometheus/prometheus-substituted.yaml" "--web.listen-address=${cfg.listenAddress}:${builtins.toString cfg.port}" "--alertmanager.notification-queue-capacity=${toString cfg.alertmanagerNotificationQueueCapacity}" "--alertmanager.timeout=${toString cfg.alertmanagerTimeout}s" - ] ++ - optional (cfg.webExternalUrl != null) "--web.external-url=${cfg.webExternalUrl}"; + ] ++ optional (cfg.webExternalUrl != null) "--web.external-url=${cfg.webExternalUrl}" + ++ optional (cfg.retentionTime != null) "--storage.tsdb.retention.time=${cfg.retentionTime}"; filterValidPrometheus = filterAttrsListRecursive (n: v: !(n == "_module" || v == null)); filterAttrsListRecursive = pred: x: @@ -101,6 +103,157 @@ let }; }; + promTypes.remote_read = types.submodule { + options = { + url = mkOption { + type = types.str; + description = '' + ServerName extension to indicate the name of the server. + http://tools.ietf.org/html/rfc4366#section-3.1 + ''; + }; + name = mkOpt types.str '' + Name of the remote read config, which if specified must be unique among remote read configs. + The name will be used in metrics and logging in place of a generated value to help users distinguish between + remote read configs. + ''; + required_matchers = mkOpt (types.attrsOf types.str) '' + An optional list of equality matchers which have to be + present in a selector to query the remote read endpoint. + ''; + remote_timeout = mkOpt types.str '' + Timeout for requests to the remote read endpoint. + ''; + read_recent = mkOpt types.bool '' + Whether reads should be made for queries for time ranges that + the local storage should have complete data for. + ''; + basic_auth = mkOpt (types.submodule { + options = { + username = mkOption { + type = types.str; + description = '' + HTTP username + ''; + }; + password = mkOpt types.str "HTTP password"; + password_file = mkOpt types.str "HTTP password file"; + }; + }) '' + Sets the `Authorization` header on every remote read request with the + configured username and password. + password and password_file are mutually exclusive. + ''; + bearer_token = mkOpt types.str '' + Sets the `Authorization` header on every remote read request with + the configured bearer token. It is mutually exclusive with `bearer_token_file`. + ''; + bearer_token_file = mkOpt types.str '' + Sets the `Authorization` header on every remote read request with the bearer token + read from the configured file. It is mutually exclusive with `bearer_token`. + ''; + tls_config = mkOpt promTypes.tls_config '' + Configures the remote read request's TLS settings. + ''; + proxy_url = mkOpt types.str "Optional Proxy URL."; + }; + }; + + promTypes.remote_write = types.submodule { + options = { + url = mkOption { + type = types.str; + description = '' + ServerName extension to indicate the name of the server. + http://tools.ietf.org/html/rfc4366#section-3.1 + ''; + }; + remote_timeout = mkOpt types.str '' + Timeout for requests to the remote write endpoint. + ''; + write_relabel_configs = mkOpt (types.listOf promTypes.relabel_config) '' + List of remote write relabel configurations. + ''; + name = mkOpt types.str '' + Name of the remote write config, which if specified must be unique among remote write configs. + The name will be used in metrics and logging in place of a generated value to help users distinguish between + remote write configs. + ''; + basic_auth = mkOpt (types.submodule { + options = { + username = mkOption { + type = types.str; + description = '' + HTTP username + ''; + }; + password = mkOpt types.str "HTTP password"; + password_file = mkOpt types.str "HTTP password file"; + }; + }) '' + Sets the `Authorization` header on every remote write request with the + configured username and password. + password and password_file are mutually exclusive. + ''; + bearer_token = mkOpt types.str '' + Sets the `Authorization` header on every remote write request with + the configured bearer token. It is mutually exclusive with `bearer_token_file`. + ''; + bearer_token_file = mkOpt types.str '' + Sets the `Authorization` header on every remote write request with the bearer token + read from the configured file. It is mutually exclusive with `bearer_token`. + ''; + tls_config = mkOpt promTypes.tls_config '' + Configures the remote write request's TLS settings. + ''; + proxy_url = mkOpt types.str "Optional Proxy URL."; + queue_config = mkOpt (types.submodule { + options = { + capacity = mkOpt types.int '' + Number of samples to buffer per shard before we block reading of more + samples from the WAL. It is recommended to have enough capacity in each + shard to buffer several requests to keep throughput up while processing + occasional slow remote requests. + ''; + max_shards = mkOpt types.int '' + Maximum number of shards, i.e. amount of concurrency. + ''; + min_shards = mkOpt types.int '' + Minimum number of shards, i.e. amount of concurrency. + ''; + max_samples_per_send = mkOpt types.int '' + Maximum number of samples per send. + ''; + batch_send_deadline = mkOpt types.str '' + Maximum time a sample will wait in buffer. + ''; + min_backoff = mkOpt types.str '' + Initial retry delay. Gets doubled for every retry. + ''; + max_backoff = mkOpt types.str '' + Maximum retry delay. + ''; + }; + }) '' + Configures the queue used to write to remote storage. + ''; + metadata_config = mkOpt (types.submodule { + options = { + send = mkOpt types.bool '' + Whether metric metadata is sent to remote storage or not. + ''; + send_interval = mkOpt types.str '' + How frequently metric metadata is sent to remote storage. + ''; + }; + }) '' + Configures the sending of series metadata to remote storage. + Metadata configuration is subject to change at any point + or be removed in future releases. + ''; + }; + }; + promTypes.scrape_config = types.submodule { options = { job_name = mkOption { @@ -170,15 +323,13 @@ let HTTP username ''; }; - password = mkOption { - type = types.str; - description = '' - HTTP password - ''; - }; + password = mkOpt types.str "HTTP password"; + password_file = mkOpt types.str "HTTP password file"; }; }) '' - Optional http login credentials for metrics scraping. + Sets the `Authorization` header on every scrape request with the + configured username and password. + password and password_file are mutually exclusive. ''; bearer_token = mkOpt types.str '' @@ -217,6 +368,14 @@ let List of file service discovery configurations. ''; + gce_sd_configs = mkOpt (types.listOf promTypes.gce_sd_config) '' + List of Google Compute Engine service discovery configurations. + + See <link + xlink:href="https://prometheus.io/docs/prometheus/latest/configuration/configuration/#gce_sd_config">the + relevant Prometheus configuration docs</link> for more detail. + ''; + static_configs = mkOpt (types.listOf promTypes.static_config) '' List of labeled target groups for this job. ''; @@ -225,6 +384,10 @@ let List of relabel configurations. ''; + metric_relabel_configs = mkOpt (types.listOf promTypes.relabel_config) '' + List of metric relabel configurations. + ''; + sample_limit = mkDefOpt types.int "0" '' Per-scrape limit on number of scraped samples that will be accepted. If more than this number of samples are present after metric relabelling @@ -307,7 +470,7 @@ let ''; }; - value = mkOption { + values = mkOption { type = types.listOf types.str; default = []; description = '' @@ -402,6 +565,52 @@ let }; }; + promTypes.gce_sd_config = types.submodule { + options = { + # Use `mkOption` instead of `mkOpt` for project and zone because they are + # required configuration values for `gce_sd_config`. + project = mkOption { + type = types.str; + description = '' + The GCP Project. + ''; + }; + + zone = mkOption { + type = types.str; + description = '' + The zone of the scrape targets. If you need multiple zones use multiple + gce_sd_configs. + ''; + }; + + filter = mkOpt types.str '' + Filter can be used optionally to filter the instance list by other + criteria Syntax of this filter string is described here in the filter + query parameter section: <link + xlink:href="https://cloud.google.com/compute/docs/reference/latest/instances/list" + />. + ''; + + refresh_interval = mkDefOpt types.str "60s" '' + Refresh interval to re-read the cloud instance list. + ''; + + port = mkDefOpt types.port "80" '' + The port to scrape metrics from. If using the public IP address, this + must instead be specified in the relabeling rule. + ''; + + tag_separator = mkDefOpt types.str "," '' + The tag separator used to separate concatenated GCE instance network tags. + + See the GCP documentation on network tags for more information: <link + xlink:href="https://cloud.google.com/vpc/docs/add-remove-network-tags" + /> + ''; + }; + }; + promTypes.relabel_config = types.submodule { options = { source_labels = mkOpt (types.listOf types.str) '' @@ -432,10 +641,10 @@ let regular expression matches. ''; - action = mkDefOpt (types.enum ["replace" "keep" "drop"]) "replace" '' + action = + mkDefOpt (types.enum ["replace" "keep" "drop" "hashmod" "labelmap" "labeldrop" "labelkeep"]) "replace" '' Action to perform based on regex matching. ''; - }; }; @@ -522,6 +731,45 @@ in { ''; }; + environmentFile = mkOption { + type = types.nullOr types.path; + default = null; + example = "/root/prometheus.env"; + description = '' + Environment file as defined in <citerefentry> + <refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum> + </citerefentry>. + + Secrets may be passed to the service without adding them to the + world-readable Nix store, by specifying placeholder variables as + the option value in Nix and setting these variables accordingly in the + environment file. + + Environment variables from this file will be interpolated into the + config file using envsubst with this syntax: + <literal>$ENVIRONMENT ''${VARIABLE}</literal> + + <programlisting> + # Example scrape config entry handling an OAuth bearer token + { + job_name = "home_assistant"; + metrics_path = "/api/prometheus"; + scheme = "https"; + bearer_token = "\''${HOME_ASSISTANT_BEARER_TOKEN}"; + [...] + } + </programlisting> + + <programlisting> + # Content of the environment file + HOME_ASSISTANT_BEARER_TOKEN=someoauthbearertoken + </programlisting> + + Note that this file needs to be available on the host on which + <literal>Prometheus</literal> is running. + ''; + }; + configText = mkOption { type = types.nullOr types.lines; default = null; @@ -541,6 +789,24 @@ in { ''; }; + remoteRead = mkOption { + type = types.listOf promTypes.remote_read; + default = []; + description = '' + Parameters of the endpoints to query from. + See <link xlink:href="https://prometheus.io/docs/prometheus/latest/configuration/configuration/#remote_read">the official documentation</link> for more information. + ''; + }; + + remoteWrite = mkOption { + type = types.listOf promTypes.remote_write; + default = []; + description = '' + Parameters of the endpoints to send samples to. + See <link xlink:href="https://prometheus.io/docs/prometheus/latest/configuration/configuration/#remote_write">the official documentation</link> for more information. + ''; + }; + rules = mkOption { type = types.listOf types.str; default = []; @@ -624,12 +890,23 @@ in { errors, despite a correct configuration. ''; }; + + retentionTime = mkOption { + type = types.nullOr types.str; + default = null; + example = "15d"; + description = '' + How long to retain samples in storage. + ''; + }; }; config = mkIf cfg.enable { assertions = [ ( let - legacy = builtins.match "(.*):(.*)" cfg.listenAddress; + # Match something with dots (an IPv4 address) or something ending in + # a square bracket (an IPv6 addresses) followed by a port number. + legacy = builtins.match "(.*\\..*|.*]):([[:digit:]]+)" cfg.listenAddress; in { assertion = legacy == null; message = '' @@ -651,14 +928,22 @@ in { systemd.services.prometheus = { wantedBy = [ "multi-user.target" ]; after = [ "network.target" ]; + preStart = '' + ${lib.getBin pkgs.envsubst}/bin/envsubst -o "/run/prometheus/prometheus-substituted.yaml" \ + -i "${prometheusYml}" + ''; serviceConfig = { ExecStart = "${cfg.package}/bin/prometheus" + optionalString (length cmdlineArgs != 0) (" \\\n " + concatStringsSep " \\\n " cmdlineArgs); User = "prometheus"; Restart = "always"; + EnvironmentFile = mkIf (cfg.environmentFile != null) [ cfg.environmentFile ]; + RuntimeDirectory = "prometheus"; + RuntimeDirectoryMode = "0700"; WorkingDirectory = workingDir; StateDirectory = cfg.stateDir; + StateDirectoryMode = "0700"; }; }; }; diff --git a/nixos/modules/services/monitoring/prometheus/exporters.nix b/nixos/modules/services/monitoring/prometheus/exporters.nix index 59748efe0de..d648de6a414 100644 --- a/nixos/modules/services/monitoring/prometheus/exporters.nix +++ b/nixos/modules/services/monitoring/prometheus/exporters.nix @@ -3,7 +3,7 @@ let inherit (lib) concatStrings foldl foldl' genAttrs literalExample maintainers mapAttrsToList mkDefault mkEnableOption mkIf mkMerge mkOption - optional types; + optional types mkOptionDefault flip attrNames; cfg = config.services.prometheus.exporters; @@ -22,14 +22,22 @@ let exporterOpts = genAttrs [ "apcupsd" + "artifactory" "bind" + "bird" + "bitcoin" "blackbox" + "buildkite-agent" "collectd" "dnsmasq" + "domain" "dovecot" "fritzbox" "json" + "jitsi" + "kea" "keylight" + "knot" "lnd" "mail" "mikrotik" @@ -37,17 +45,31 @@ let "modemmanager" "nextcloud" "nginx" + "nginxlog" "node" + "openldap" + "openvpn" + "pihole" "postfix" "postgres" + "process" + "py-air-control" "redis" "rspamd" + "rtl_433" + "script" "snmp" + "smokeping" + "sql" "surfboard" + "systemd" "tor" + "unbound" "unifi" + "unifi-poller" "varnish" "wireguard" + "flow" ] (name: import (./. + "/exporters/${name}.nix") { inherit config lib pkgs options; } ); @@ -55,7 +77,7 @@ let mkExporterOpts = ({ name, port }: { enable = mkEnableOption "the prometheus ${name} exporter"; port = mkOption { - type = types.int; + type = types.port; default = port; description = '' Port to listen on. @@ -83,8 +105,8 @@ let ''; }; firewallFilter = mkOption { - type = types.str; - default = "-p tcp -m tcp --dport ${toString port}"; + type = types.nullOr types.str; + default = null; example = literalExample '' "-i eth0 -p tcp -m tcp --dport ${toString port}" ''; @@ -99,7 +121,6 @@ let default = "${name}-exporter"; description = '' User name under which the ${name} exporter shall be run. - Has no effect when <option>systemd.services.prometheus-${name}-exporter.serviceConfig.DynamicUser</option> is true. ''; }; group = mkOption { @@ -107,19 +128,20 @@ let default = "${name}-exporter"; description = '' Group under which the ${name} exporter shall be run. - Has no effect when <option>systemd.services.prometheus-${name}-exporter.serviceConfig.DynamicUser</option> is true. ''; }; }); mkSubModule = { name, port, extraOpts, imports }: { ${name} = mkOption { - type = types.submodule { + type = types.submodule [{ inherit imports; options = (mkExporterOpts { inherit name port; } // extraOpts); - }; + } ({ config, ... }: mkIf config.openFirewall { + firewallFilter = mkDefault "-p tcp -m tcp --dport ${toString config.port}"; + })]; internal = true; default = {}; }; @@ -159,10 +181,9 @@ let serviceConfig.PrivateTmp = mkDefault true; serviceConfig.WorkingDirectory = mkDefault /tmp; serviceConfig.DynamicUser = mkDefault enableDynamicUser; - } serviceOpts ] ++ optional (!enableDynamicUser) { - serviceConfig.User = conf.user; + serviceConfig.User = mkDefault conf.user; serviceConfig.Group = conf.group; - }); + } serviceOpts ]); }; in { @@ -217,16 +238,29 @@ in Please specify either 'services.prometheus.exporters.mail.configuration' or 'services.prometheus.exporters.mail.configFile'. ''; - } ]; + } { + assertion = cfg.sql.enable -> ( + (cfg.sql.configFile == null) != (cfg.sql.configuration == null) + ); + message = '' + Please specify either 'services.prometheus.exporters.sql.configuration' or + 'services.prometheus.exporters.sql.configFile' + ''; + } ] ++ (flip map (attrNames cfg) (exporter: { + assertion = cfg.${exporter}.firewallFilter != null -> cfg.${exporter}.openFirewall; + message = '' + The `firewallFilter'-option of exporter ${exporter} doesn't have any effect unless + `openFirewall' is set to `true'! + ''; + })); }] ++ [(mkIf config.services.minio.enable { services.prometheus.exporters.minio.minioAddress = mkDefault "http://localhost:9000"; services.prometheus.exporters.minio.minioAccessKey = mkDefault config.services.minio.accessKey; services.prometheus.exporters.minio.minioAccessSecret = mkDefault config.services.minio.secretKey; - })] ++ [(mkIf config.services.rspamd.enable { - services.prometheus.exporters.rspamd.url = mkDefault "http://localhost:11334/stat"; - })] ++ [(mkIf config.services.nginx.enable { - systemd.services.prometheus-nginx-exporter.after = [ "nginx.service" ]; - systemd.services.prometheus-nginx-exporter.requires = [ "nginx.service" ]; + })] ++ [(mkIf config.services.prometheus.exporters.rtl_433.enable { + hardware.rtl-sdr.enable = mkDefault true; + })] ++ [(mkIf config.services.postfix.enable { + services.prometheus.exporters.postfix.group = mkDefault config.services.postfix.setgidGroup; })] ++ (mapAttrsToList (name: conf: mkExporterConf { inherit name; diff --git a/nixos/modules/services/monitoring/prometheus/exporters/artifactory.nix b/nixos/modules/services/monitoring/prometheus/exporters/artifactory.nix new file mode 100644 index 00000000000..2adcecc728b --- /dev/null +++ b/nixos/modules/services/monitoring/prometheus/exporters/artifactory.nix @@ -0,0 +1,59 @@ +{ config, lib, pkgs, options }: + +with lib; + +let + cfg = config.services.prometheus.exporters.artifactory; +in +{ + port = 9531; + extraOpts = { + scrapeUri = mkOption { + type = types.str; + default = "http://localhost:8081/artifactory"; + description = '' + URI on which to scrape JFrog Artifactory. + ''; + }; + + artiUsername = mkOption { + type = types.str; + description = '' + Username for authentication against JFrog Artifactory API. + ''; + }; + + artiPassword = mkOption { + type = types.str; + default = ""; + description = '' + Password for authentication against JFrog Artifactory API. + One of the password or access token needs to be set. + ''; + }; + + artiAccessToken = mkOption { + type = types.str; + default = ""; + description = '' + Access token for authentication against JFrog Artifactory API. + One of the password or access token needs to be set. + ''; + }; + }; + serviceOpts = { + serviceConfig = { + ExecStart = '' + ${pkgs.prometheus-artifactory-exporter}/bin/artifactory_exporter \ + --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \ + --artifactory.scrape-uri ${cfg.scrapeUri} \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + Environment = [ + "ARTI_USERNAME=${cfg.artiUsername}" + "ARTI_PASSWORD=${cfg.artiPassword}" + "ARTI_ACCESS_TOKEN=${cfg.artiAccessToken}" + ]; + }; + }; +} diff --git a/nixos/modules/services/monitoring/prometheus/exporters/bind.nix b/nixos/modules/services/monitoring/prometheus/exporters/bind.nix index 972632b5a24..16c2920751d 100644 --- a/nixos/modules/services/monitoring/prometheus/exporters/bind.nix +++ b/nixos/modules/services/monitoring/prometheus/exporters/bind.nix @@ -41,12 +41,12 @@ in serviceConfig = { ExecStart = '' ${pkgs.prometheus-bind-exporter}/bin/bind_exporter \ - -web.listen-address ${cfg.listenAddress}:${toString cfg.port} \ - -bind.pid-file /var/run/named/named.pid \ - -bind.timeout ${toString cfg.bindTimeout} \ - -bind.stats-url ${cfg.bindURI} \ - -bind.stats-version ${cfg.bindVersion} \ - -bind.stats-groups ${concatStringsSep "," cfg.bindGroups} \ + --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \ + --bind.pid-file /var/run/named/named.pid \ + --bind.timeout ${toString cfg.bindTimeout} \ + --bind.stats-url ${cfg.bindURI} \ + --bind.stats-version ${cfg.bindVersion} \ + --bind.stats-groups ${concatStringsSep "," cfg.bindGroups} \ ${concatStringsSep " \\\n " cfg.extraFlags} ''; }; diff --git a/nixos/modules/services/monitoring/prometheus/exporters/bird.nix b/nixos/modules/services/monitoring/prometheus/exporters/bird.nix new file mode 100644 index 00000000000..d8a526eafce --- /dev/null +++ b/nixos/modules/services/monitoring/prometheus/exporters/bird.nix @@ -0,0 +1,46 @@ +{ config, lib, pkgs, options }: + +with lib; + +let + cfg = config.services.prometheus.exporters.bird; +in +{ + port = 9324; + extraOpts = { + birdVersion = mkOption { + type = types.enum [ 1 2 ]; + default = 2; + description = '' + Specifies whether BIRD1 or BIRD2 is in use. + ''; + }; + birdSocket = mkOption { + type = types.path; + default = "/var/run/bird.ctl"; + description = '' + Path to BIRD2 (or BIRD1 v4) socket. + ''; + }; + newMetricFormat = mkOption { + type = types.bool; + default = true; + description = '' + Enable the new more-generic metric format. + ''; + }; + }; + serviceOpts = { + serviceConfig = { + SupplementaryGroups = singleton (if cfg.birdVersion == 1 then "bird" else "bird2"); + ExecStart = '' + ${pkgs.prometheus-bird-exporter}/bin/bird_exporter \ + -web.listen-address ${cfg.listenAddress}:${toString cfg.port} \ + -bird.socket ${cfg.birdSocket} \ + -bird.v2=${if cfg.birdVersion == 2 then "true" else "false"} \ + -format.new=${if cfg.newMetricFormat then "true" else "false"} \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + }; + }; +} diff --git a/nixos/modules/services/monitoring/prometheus/exporters/bitcoin.nix b/nixos/modules/services/monitoring/prometheus/exporters/bitcoin.nix new file mode 100644 index 00000000000..43721f70b49 --- /dev/null +++ b/nixos/modules/services/monitoring/prometheus/exporters/bitcoin.nix @@ -0,0 +1,82 @@ +{ config, lib, pkgs, options }: + +with lib; + +let + cfg = config.services.prometheus.exporters.bitcoin; +in +{ + port = 9332; + extraOpts = { + rpcUser = mkOption { + type = types.str; + default = "bitcoinrpc"; + description = '' + RPC user name. + ''; + }; + + rpcPasswordFile = mkOption { + type = types.path; + description = '' + File containing RPC password. + ''; + }; + + rpcScheme = mkOption { + type = types.enum [ "http" "https" ]; + default = "http"; + description = '' + Whether to connect to bitcoind over http or https. + ''; + }; + + rpcHost = mkOption { + type = types.str; + default = "localhost"; + description = '' + RPC host. + ''; + }; + + rpcPort = mkOption { + type = types.port; + default = 8332; + description = '' + RPC port number. + ''; + }; + + refreshSeconds = mkOption { + type = types.ints.unsigned; + default = 300; + description = '' + How often to ask bitcoind for metrics. + ''; + }; + + extraEnv = mkOption { + type = types.attrsOf types.str; + default = {}; + description = '' + Extra environment variables for the exporter. + ''; + }; + }; + serviceOpts = { + script = '' + export BITCOIN_RPC_PASSWORD=$(cat ${cfg.rpcPasswordFile}) + exec ${pkgs.prometheus-bitcoin-exporter}/bin/bitcoind-monitor.py + ''; + + environment = { + BITCOIN_RPC_USER = cfg.rpcUser; + BITCOIN_RPC_SCHEME = cfg.rpcScheme; + BITCOIN_RPC_HOST = cfg.rpcHost; + BITCOIN_RPC_PORT = toString cfg.rpcPort; + METRICS_ADDR = cfg.listenAddress; + METRICS_PORT = toString cfg.port; + REFRESH_SECONDS = toString cfg.refreshSeconds; + } // cfg.extraEnv; + }; +} diff --git a/nixos/modules/services/monitoring/prometheus/exporters/buildkite-agent.nix b/nixos/modules/services/monitoring/prometheus/exporters/buildkite-agent.nix new file mode 100644 index 00000000000..7557480ac06 --- /dev/null +++ b/nixos/modules/services/monitoring/prometheus/exporters/buildkite-agent.nix @@ -0,0 +1,64 @@ +{ config, lib, pkgs, options }: + +with lib; + +let + cfg = config.services.prometheus.exporters.buildkite-agent; +in +{ + port = 9876; + extraOpts = { + tokenPath = mkOption { + type = types.nullOr types.path; + apply = final: if final == null then null else toString final; + description = '' + The token from your Buildkite "Agents" page. + + A run-time path to the token file, which is supposed to be provisioned + outside of Nix store. + ''; + }; + interval = mkOption { + type = types.str; + default = "30s"; + example = "1min"; + description = '' + How often to update metrics. + ''; + }; + endpoint = mkOption { + type = types.str; + default = "https://agent.buildkite.com/v3"; + description = '' + The Buildkite Agent API endpoint. + ''; + }; + queues = mkOption { + type = with types; nullOr (listOf str); + default = null; + example = literalExample ''[ "my-queue1" "my-queue2" ]''; + description = '' + Which specific queues to process. + ''; + }; + }; + serviceOpts = { + script = + let + queues = concatStringsSep " " (map (q: "-queue ${q}") cfg.queues); + in + '' + export BUILDKITE_AGENT_TOKEN="$(cat ${toString cfg.tokenPath})" + exec ${pkgs.buildkite-agent-metrics}/bin/buildkite-agent-metrics \ + -backend prometheus \ + -interval ${cfg.interval} \ + -endpoint ${cfg.endpoint} \ + ${optionalString (cfg.queues != null) queues} \ + -prometheus-addr "${cfg.listenAddress}:${toString cfg.port}" ${concatStringsSep " " cfg.extraFlags} + ''; + serviceConfig = { + DynamicUser = false; + RuntimeDirectory = "buildkite-agent-metrics"; + }; + }; +} diff --git a/nixos/modules/services/monitoring/prometheus/exporters/collectd.nix b/nixos/modules/services/monitoring/prometheus/exporters/collectd.nix index 97210463027..a7f4d3e096f 100644 --- a/nixos/modules/services/monitoring/prometheus/exporters/collectd.nix +++ b/nixos/modules/services/monitoring/prometheus/exporters/collectd.nix @@ -20,7 +20,7 @@ in port = mkOption { type = types.int; default = 25826; - description = ''Network address on which to accept collectd binary network packets.''; + description = "Network address on which to accept collectd binary network packets."; }; listenAddress = mkOption { @@ -41,11 +41,11 @@ in }; logFormat = mkOption { - type = types.str; - default = "logger:stderr"; - example = "logger:syslog?appname=bob&local=7 or logger:stdout?json=true"; + type = types.enum [ "logfmt" "json" ]; + default = "logfmt"; + example = "json"; description = '' - Set the log target and format. + Set the log format. ''; }; @@ -59,16 +59,16 @@ in }; serviceOpts = let collectSettingsArgs = if (cfg.collectdBinary.enable) then '' - -collectd.listen-address ${cfg.collectdBinary.listenAddress}:${toString cfg.collectdBinary.port} \ - -collectd.security-level ${cfg.collectdBinary.securityLevel} \ + --collectd.listen-address ${cfg.collectdBinary.listenAddress}:${toString cfg.collectdBinary.port} \ + --collectd.security-level ${cfg.collectdBinary.securityLevel} \ '' else ""; in { serviceConfig = { ExecStart = '' ${pkgs.prometheus-collectd-exporter}/bin/collectd_exporter \ - -log.format ${escapeShellArg cfg.logFormat} \ - -log.level ${cfg.logLevel} \ - -web.listen-address ${cfg.listenAddress}:${toString cfg.port} \ + --log.format ${escapeShellArg cfg.logFormat} \ + --log.level ${cfg.logLevel} \ + --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \ ${collectSettingsArgs} \ ${concatStringsSep " \\\n " cfg.extraFlags} ''; diff --git a/nixos/modules/services/monitoring/prometheus/exporters/domain.nix b/nixos/modules/services/monitoring/prometheus/exporters/domain.nix new file mode 100644 index 00000000000..61e2fc80afd --- /dev/null +++ b/nixos/modules/services/monitoring/prometheus/exporters/domain.nix @@ -0,0 +1,19 @@ +{ config, lib, pkgs, options }: + +with lib; + +let + cfg = config.services.prometheus.exporters.domain; +in +{ + port = 9222; + serviceOpts = { + serviceConfig = { + ExecStart = '' + ${pkgs.prometheus-domain-exporter}/bin/domain_exporter \ + --bind ${cfg.listenAddress}:${toString cfg.port} \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + }; + }; +} diff --git a/nixos/modules/services/monitoring/prometheus/exporters/dovecot.nix b/nixos/modules/services/monitoring/prometheus/exporters/dovecot.nix index aba3533e439..472652fe8a7 100644 --- a/nixos/modules/services/monitoring/prometheus/exporters/dovecot.nix +++ b/nixos/modules/services/monitoring/prometheus/exporters/dovecot.nix @@ -35,13 +35,28 @@ in { <xref linkend="opt-services.prometheus.exporters.dovecot.enable" /> = true; <xref linkend="opt-services.prometheus.exporters.dovecot.socketPath" /> = "/var/run/dovecot2/old-stats"; + <xref linkend="opt-services.dovecot2.mailPlugins.globally.enable" /> = [ "old_stats" ]; <xref linkend="opt-services.dovecot2.extraConfig" /> = ''' - mail_plugins = $mail_plugins old_stats service old-stats { unix_listener old-stats { user = dovecot-exporter group = dovecot-exporter + mode = 0660 } + fifo_listener old-stats-mail { + mode = 0660 + user = dovecot + group = dovecot + } + fifo_listener old-stats-user { + mode = 0660 + user = dovecot + group = dovecot + } + } + plugin { + old_stats_refresh = 30 secs + old_stats_track_cmds = yes } '''; } diff --git a/nixos/modules/services/monitoring/prometheus/exporters/flow.nix b/nixos/modules/services/monitoring/prometheus/exporters/flow.nix new file mode 100644 index 00000000000..6a35f46308f --- /dev/null +++ b/nixos/modules/services/monitoring/prometheus/exporters/flow.nix @@ -0,0 +1,50 @@ +{ config, lib, pkgs, options }: + +with lib; + +let + cfg = config.services.prometheus.exporters.flow; +in { + port = 9590; + extraOpts = { + brokers = mkOption { + type = types.listOf types.str; + example = literalExample ''[ "kafka.example.org:19092" ]''; + description = "List of Kafka brokers to connect to."; + }; + + asn = mkOption { + type = types.ints.positive; + example = 65542; + description = "The ASN being monitored."; + }; + + partitions = mkOption { + type = types.listOf types.int; + default = []; + description = '' + The number of the partitions to consume, none means all. + ''; + }; + + topic = mkOption { + type = types.str; + example = "pmacct.acct"; + description = "The Kafka topic to consume from."; + }; + }; + + serviceOpts = { + serviceConfig = { + DynamicUser = true; + ExecStart = '' + ${pkgs.prometheus-flow-exporter}/bin/flow-exporter \ + -asn ${toString cfg.asn} \ + -topic ${cfg.topic} \ + -brokers ${concatStringsSep "," cfg.brokers} \ + ${optionalString (cfg.partitions != []) "-partitions ${concatStringsSep "," cfg.partitions}"} \ + -addr ${cfg.listenAddress}:${toString cfg.port} ${concatStringsSep " " cfg.extraFlags} + ''; + }; + }; +} diff --git a/nixos/modules/services/monitoring/prometheus/exporters/jitsi.nix b/nixos/modules/services/monitoring/prometheus/exporters/jitsi.nix new file mode 100644 index 00000000000..c93a8f98e55 --- /dev/null +++ b/nixos/modules/services/monitoring/prometheus/exporters/jitsi.nix @@ -0,0 +1,40 @@ +{ config, lib, pkgs, options }: + +with lib; + +let + cfg = config.services.prometheus.exporters.jitsi; +in +{ + port = 9700; + extraOpts = { + url = mkOption { + type = types.str; + default = "http://localhost:8080/colibri/stats"; + description = '' + Jitsi Videobridge metrics URL to monitor. + This is usually /colibri/stats on port 8080 of the jitsi videobridge host. + ''; + }; + interval = mkOption { + type = types.str; + default = "30s"; + example = "1min"; + description = '' + How often to scrape new data + ''; + }; + }; + serviceOpts = { + serviceConfig = { + ExecStart = '' + ${pkgs.prometheus-jitsi-exporter}/bin/jitsiexporter \ + -url ${escapeShellArg cfg.url} \ + -host ${cfg.listenAddress} \ + -port ${toString cfg.port} \ + -interval ${toString cfg.interval} \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + }; + }; +} diff --git a/nixos/modules/services/monitoring/prometheus/exporters/json.nix b/nixos/modules/services/monitoring/prometheus/exporters/json.nix index bd0026b55f7..1800da69a25 100644 --- a/nixos/modules/services/monitoring/prometheus/exporters/json.nix +++ b/nixos/modules/services/monitoring/prometheus/exporters/json.nix @@ -8,28 +8,36 @@ in { port = 7979; extraOpts = { - url = mkOption { - type = types.str; - description = '' - URL to scrape JSON from. - ''; - }; configFile = mkOption { type = types.path; description = '' Path to configuration file. ''; }; - listenAddress = {}; # not used }; serviceOpts = { serviceConfig = { ExecStart = '' - ${pkgs.prometheus-json-exporter}/bin/prometheus-json-exporter \ - --port ${toString cfg.port} \ - ${cfg.url} ${escapeShellArg cfg.configFile} \ + ${pkgs.prometheus-json-exporter}/bin/json_exporter \ + --config.file ${escapeShellArg cfg.configFile} \ + --web.listen-address="${cfg.listenAddress}:${toString cfg.port}" \ ${concatStringsSep " \\\n " cfg.extraFlags} ''; }; }; + imports = [ + (mkRemovedOptionModule [ "url" ] '' + This option was removed. The URL of the endpoint serving JSON + must now be provided to the exporter by prometheus via the url + parameter `target'. + + In prometheus a scrape URL would look like this: + + http://some.json-exporter.host:7979/probe?target=https://example.com/some/json/endpoint + + For more information, take a look at the official documentation + (https://github.com/prometheus-community/json_exporter) of the json_exporter. + '') + ({ options.warnings = options.warnings; options.assertions = options.assertions; }) + ]; } diff --git a/nixos/modules/services/monitoring/prometheus/exporters/kea.nix b/nixos/modules/services/monitoring/prometheus/exporters/kea.nix new file mode 100644 index 00000000000..9677281f877 --- /dev/null +++ b/nixos/modules/services/monitoring/prometheus/exporters/kea.nix @@ -0,0 +1,39 @@ +{ config +, lib +, pkgs +, options +}: + +with lib; + +let + cfg = config.services.prometheus.exporters.kea; +in { + port = 9547; + extraOpts = { + controlSocketPaths = mkOption { + type = types.listOf types.str; + example = literalExample '' + [ + "/run/kea/kea-dhcp4.socket" + "/run/kea/kea-dhcp6.socket" + ] + ''; + description = '' + Paths to kea control sockets + ''; + }; + }; + serviceOpts = { + serviceConfig = { + User = "kea"; + ExecStart = '' + ${pkgs.prometheus-kea-exporter}/bin/kea-exporter \ + --address ${cfg.listenAddress} \ + --port ${toString cfg.port} \ + ${concatStringsSep " \\n" cfg.controlSocketPaths} + ''; + SupplementaryGroups = [ "kea" ]; + }; + }; +} diff --git a/nixos/modules/services/monitoring/prometheus/exporters/knot.nix b/nixos/modules/services/monitoring/prometheus/exporters/knot.nix new file mode 100644 index 00000000000..46c28fe0a57 --- /dev/null +++ b/nixos/modules/services/monitoring/prometheus/exporters/knot.nix @@ -0,0 +1,50 @@ +{ config, lib, pkgs, options }: + +with lib; + +let + cfg = config.services.prometheus.exporters.knot; +in { + port = 9433; + extraOpts = { + knotLibraryPath = mkOption { + type = types.str; + default = "${pkgs.knot-dns.out}/lib/libknot.so"; + defaultText = "\${pkgs.knot-dns}/lib/libknot.so"; + description = '' + Path to the library of <package>knot-dns</package>. + ''; + }; + + knotSocketPath = mkOption { + type = types.str; + default = "/run/knot/knot.sock"; + description = '' + Socket path of <citerefentry><refentrytitle>knotd</refentrytitle> + <manvolnum>8</manvolnum></citerefentry>. + ''; + }; + + knotSocketTimeout = mkOption { + type = types.int; + default = 2000; + description = '' + Timeout in seconds. + ''; + }; + }; + serviceOpts = { + serviceConfig = { + ExecStart = '' + ${pkgs.prometheus-knot-exporter}/bin/knot_exporter \ + --web-listen-addr ${cfg.listenAddress} \ + --web-listen-port ${toString cfg.port} \ + --knot-library-path ${cfg.knotLibraryPath} \ + --knot-socket-path ${cfg.knotSocketPath} \ + --knot-socket-timeout ${toString cfg.knotSocketTimeout} \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + SupplementaryGroups = [ "knot" ]; + }; + }; +} diff --git a/nixos/modules/services/monitoring/prometheus/exporters/mail.nix b/nixos/modules/services/monitoring/prometheus/exporters/mail.nix index 18c5c4dd162..7e196149fbb 100644 --- a/nixos/modules/services/monitoring/prometheus/exporters/mail.nix +++ b/nixos/modules/services/monitoring/prometheus/exporters/mail.nix @@ -112,6 +112,24 @@ let ''; description = '' List of servers that should be probed. + + <emphasis>Note:</emphasis> if your mailserver has <citerefentry> + <refentrytitle>rspamd</refentrytitle><manvolnum>8</manvolnum></citerefentry> configured, + it can happen that emails from this exporter are marked as spam. + + It's possible to work around the issue with a config like this: + <programlisting> + { + <link linkend="opt-services.rspamd.locals._name_.text">services.rspamd.locals."multimap.conf".text</link> = ''' + ALLOWLIST_PROMETHEUS { + filter = "email:domain:tld"; + type = "from"; + map = "''${pkgs.writeText "allowmap" "domain.tld"}"; + score = -100.0; + } + '''; + } + </programlisting> ''; }; }; diff --git a/nixos/modules/services/monitoring/prometheus/exporters/nextcloud.nix b/nixos/modules/services/monitoring/prometheus/exporters/nextcloud.nix index aee6bd5e66c..ce7125bf5a8 100644 --- a/nixos/modules/services/monitoring/prometheus/exporters/nextcloud.nix +++ b/nixos/modules/services/monitoring/prometheus/exporters/nextcloud.nix @@ -46,11 +46,11 @@ in DynamicUser = false; ExecStart = '' ${pkgs.prometheus-nextcloud-exporter}/bin/nextcloud-exporter \ - -a ${cfg.listenAddress}:${toString cfg.port} \ - -u ${cfg.username} \ - -t ${cfg.timeout} \ - -l ${cfg.url} \ - -p ${escapeShellArg "@${cfg.passwordFile}"} \ + --addr ${cfg.listenAddress}:${toString cfg.port} \ + --username ${cfg.username} \ + --timeout ${cfg.timeout} \ + --server ${cfg.url} \ + --password ${escapeShellArg "@${cfg.passwordFile}"} \ ${concatStringsSep " \\\n " cfg.extraFlags} ''; }; diff --git a/nixos/modules/services/monitoring/prometheus/exporters/nginx.nix b/nixos/modules/services/monitoring/prometheus/exporters/nginx.nix index 56cddfc55b7..5ee8c346be1 100644 --- a/nixos/modules/services/monitoring/prometheus/exporters/nginx.nix +++ b/nixos/modules/services/monitoring/prometheus/exporters/nginx.nix @@ -42,7 +42,7 @@ in ''; }; }; - serviceOpts = { + serviceOpts = mkMerge ([{ serviceConfig = { ExecStart = '' ${pkgs.prometheus-nginx-exporter}/bin/nginx-prometheus-exporter \ @@ -54,7 +54,10 @@ in ${concatStringsSep " \\\n " cfg.extraFlags} ''; }; - }; + }] ++ [(mkIf config.services.nginx.enable { + after = [ "nginx.service" ]; + requires = [ "nginx.service" ]; + })]); imports = [ (mkRenamedOptionModule [ "telemetryEndpoint" ] [ "telemetryPath" ]) (mkRemovedOptionModule [ "insecure" ] '' diff --git a/nixos/modules/services/monitoring/prometheus/exporters/nginxlog.nix b/nixos/modules/services/monitoring/prometheus/exporters/nginxlog.nix new file mode 100644 index 00000000000..8c1f552d58a --- /dev/null +++ b/nixos/modules/services/monitoring/prometheus/exporters/nginxlog.nix @@ -0,0 +1,51 @@ +{ config, lib, pkgs, options }: + +with lib; + +let + cfg = config.services.prometheus.exporters.nginxlog; +in { + port = 9117; + extraOpts = { + settings = mkOption { + type = types.attrs; + default = {}; + description = '' + All settings of nginxlog expressed as an Nix attrset. + + Check the official documentation for the corresponding YAML + settings that can all be used here: https://github.com/martin-helmich/prometheus-nginxlog-exporter + + The `listen` object is already generated by `port`, `listenAddress` and `metricsEndpoint` and + will be merged with the value of `settings` before writting it as JSON. + ''; + }; + + metricsEndpoint = mkOption { + type = types.str; + default = "/metrics"; + description = '' + Path under which to expose metrics. + ''; + }; + }; + + serviceOpts = let + listenConfig = { + listen = { + port = cfg.port; + address = cfg.listenAddress; + metrics_endpoint = cfg.metricsEndpoint; + }; + }; + completeConfig = pkgs.writeText "nginxlog-exporter.yaml" (builtins.toJSON (lib.recursiveUpdate listenConfig cfg.settings)); + in { + serviceConfig = { + ExecStart = '' + ${pkgs.prometheus-nginxlog-exporter}/bin/prometheus-nginxlog-exporter -config-file ${completeConfig} + ''; + Restart="always"; + ProtectSystem="full"; + }; + }; +} diff --git a/nixos/modules/services/monitoring/prometheus/exporters/openldap.nix b/nixos/modules/services/monitoring/prometheus/exporters/openldap.nix new file mode 100644 index 00000000000..888611ee6fa --- /dev/null +++ b/nixos/modules/services/monitoring/prometheus/exporters/openldap.nix @@ -0,0 +1,67 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.prometheus.exporters.openldap; +in { + port = 9330; + extraOpts = { + ldapCredentialFile = mkOption { + type = types.path; + example = "/run/keys/ldap_pass"; + description = '' + Environment file to contain the credentials to authenticate against + <package>openldap</package>. + + The file should look like this: + <programlisting> + --- + ldapUser: "cn=monitoring,cn=Monitor" + ldapPass: "secret" + </programlisting> + ''; + }; + protocol = mkOption { + default = "tcp"; + example = "udp"; + type = types.str; + description = '' + Which protocol to use to connect against <package>openldap</package>. + ''; + }; + ldapAddr = mkOption { + default = "localhost:389"; + type = types.str; + description = '' + Address of the <package>openldap</package>-instance. + ''; + }; + metricsPath = mkOption { + default = "/metrics"; + type = types.str; + description = '' + URL path where metrics should be exposed. + ''; + }; + interval = mkOption { + default = "30s"; + type = types.str; + example = "1m"; + description = '' + Scrape interval of the exporter. + ''; + }; + }; + serviceOpts.serviceConfig = { + ExecStart = '' + ${pkgs.prometheus-openldap-exporter}/bin/openldap_exporter \ + --promAddr ${cfg.listenAddress}:${toString cfg.port} \ + --metrPath ${cfg.metricsPath} \ + --ldapNet ${cfg.protocol} \ + --interval ${cfg.interval} \ + --config ${cfg.ldapCredentialFile} \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + }; +} diff --git a/nixos/modules/services/monitoring/prometheus/exporters/openvpn.nix b/nixos/modules/services/monitoring/prometheus/exporters/openvpn.nix new file mode 100644 index 00000000000..a97a753ebc3 --- /dev/null +++ b/nixos/modules/services/monitoring/prometheus/exporters/openvpn.nix @@ -0,0 +1,39 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + cfg = config.services.prometheus.exporters.openvpn; +in { + port = 9176; + extraOpts = { + statusPaths = mkOption { + type = types.listOf types.str; + description = '' + Paths to OpenVPN status files. Please configure the OpenVPN option + <literal>status</literal> accordingly. + ''; + }; + telemetryPath = mkOption { + type = types.str; + default = "/metrics"; + description = '' + Path under which to expose metrics. + ''; + }; + }; + + serviceOpts = { + serviceConfig = { + PrivateDevices = true; + ProtectKernelModules = true; + NoNewPrivileges = true; + ExecStart = '' + ${pkgs.prometheus-openvpn-exporter}/bin/openvpn_exporter \ + -openvpn.status_paths "${concatStringsSep "," cfg.statusPaths}" \ + -web.listen-address ${cfg.listenAddress}:${toString cfg.port} \ + -web.telemetry-path ${cfg.telemetryPath} + ''; + }; + }; +} diff --git a/nixos/modules/services/monitoring/prometheus/exporters/pihole.nix b/nixos/modules/services/monitoring/prometheus/exporters/pihole.nix new file mode 100644 index 00000000000..21c2e5eab4c --- /dev/null +++ b/nixos/modules/services/monitoring/prometheus/exporters/pihole.nix @@ -0,0 +1,74 @@ +{ config, lib, pkgs, options }: + +with lib; + +let + cfg = config.services.prometheus.exporters.pihole; +in +{ + port = 9617; + extraOpts = { + apiToken = mkOption { + type = types.str; + default = ""; + example = "580a770cb40511eb85290242ac130003580a770cb40511eb85290242ac130003"; + description = '' + pi-hole API token which can be used instead of a password + ''; + }; + interval = mkOption { + type = types.str; + default = "10s"; + example = "30s"; + description = '' + How often to scrape new data + ''; + }; + password = mkOption { + type = types.str; + default = ""; + example = "password"; + description = '' + The password to login into pihole. An api token can be used instead. + ''; + }; + piholeHostname = mkOption { + type = types.str; + default = "pihole"; + example = "127.0.0.1"; + description = '' + Hostname or address where to find the pihole webinterface + ''; + }; + piholePort = mkOption { + type = types.port; + default = "80"; + example = "443"; + description = '' + The port pihole webinterface is reachable on + ''; + }; + protocol = mkOption { + type = types.enum [ "http" "https" ]; + default = "http"; + example = "https"; + description = '' + The protocol which is used to connect to pihole + ''; + }; + }; + serviceOpts = { + serviceConfig = { + ExecStart = '' + ${pkgs.bash}/bin/bash -c "${pkgs.prometheus-pihole-exporter}/bin/pihole-exporter \ + -interval ${cfg.interval} \ + ${optionalString (cfg.apiToken != "") "-pihole_api_token ${cfg.apiToken}"} \ + -pihole_hostname ${cfg.piholeHostname} \ + ${optionalString (cfg.password != "") "-pihole_password ${cfg.password}"} \ + -pihole_port ${toString cfg.piholePort} \ + -pihole_protocol ${cfg.protocol} \ + -port ${toString cfg.port}" + ''; + }; + }; +} diff --git a/nixos/modules/services/monitoring/prometheus/exporters/postfix.nix b/nixos/modules/services/monitoring/prometheus/exporters/postfix.nix index 3b6ef1631f8..f57589a59c7 100644 --- a/nixos/modules/services/monitoring/prometheus/exporters/postfix.nix +++ b/nixos/modules/services/monitoring/prometheus/exporters/postfix.nix @@ -8,6 +8,15 @@ in { port = 9154; extraOpts = { + group = mkOption { + type = types.str; + description = '' + Group under which the postfix exporter shall be run. + It should match the group that is allowed to access the + <literal>showq</literal> socket in the <literal>queue/public/</literal> directory. + Defaults to <literal>services.postfix.setgidGroup</literal> when postfix is enabled. + ''; + }; telemetryPath = mkOption { type = types.str; default = "/metrics"; @@ -26,16 +35,20 @@ in }; showqPath = mkOption { type = types.path; - default = "/var/spool/postfix/public/showq"; - example = "/var/lib/postfix/queue/public/showq"; + default = "/var/lib/postfix/queue/public/showq"; + example = "/var/spool/postfix/public/showq"; description = '' - Path where Postfix places it's showq socket. + Path where Postfix places its showq socket. ''; }; systemd = { - enable = mkEnableOption '' - reading metrics from the systemd-journal instead of from a logfile - ''; + enable = mkOption { + type = types.bool; + default = true; + description = '' + Whether to enable reading metrics from the systemd journal instead of from a logfile + ''; + }; unit = mkOption { type = types.str; default = "postfix.service"; diff --git a/nixos/modules/services/monitoring/prometheus/exporters/postgres.nix b/nixos/modules/services/monitoring/prometheus/exporters/postgres.nix index 1ece73a1159..dd3bec8ec16 100644 --- a/nixos/modules/services/monitoring/prometheus/exporters/postgres.nix +++ b/nixos/modules/services/monitoring/prometheus/exporters/postgres.nix @@ -30,12 +30,49 @@ in Whether to run the exporter as the local 'postgres' super user. ''; }; + + # TODO perhaps LoadCredential would be more appropriate + environmentFile = mkOption { + type = types.nullOr types.path; + default = null; + example = "/root/prometheus-postgres-exporter.env"; + description = '' + Environment file as defined in <citerefentry> + <refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum> + </citerefentry>. + + Secrets may be passed to the service without adding them to the + world-readable Nix store, by specifying placeholder variables as + the option value in Nix and setting these variables accordingly in the + environment file. + + Environment variables from this file will be interpolated into the + config file using envsubst with this syntax: + <literal>$ENVIRONMENT ''${VARIABLE}</literal> + + The main use is to set the DATA_SOURCE_NAME that contains the + postgres password + + note that contents from this file will override dataSourceName + if you have set it from nix. + + <programlisting> + # Content of the environment file + DATA_SOURCE_NAME=postgresql://username:password@localhost:5432/postgres?sslmode=disable + </programlisting> + + Note that this file needs to be available on the host on which + this exporter is running. + ''; + }; + }; serviceOpts = { environment.DATA_SOURCE_NAME = cfg.dataSourceName; serviceConfig = { DynamicUser = false; User = mkIf cfg.runAsLocalSuperUser (mkForce "postgres"); + EnvironmentFile = mkIf (cfg.environmentFile != null) [ cfg.environmentFile ]; ExecStart = '' ${pkgs.prometheus-postgres-exporter}/bin/postgres_exporter \ --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \ diff --git a/nixos/modules/services/monitoring/prometheus/exporters/process.nix b/nixos/modules/services/monitoring/prometheus/exporters/process.nix new file mode 100644 index 00000000000..e3b3d18367f --- /dev/null +++ b/nixos/modules/services/monitoring/prometheus/exporters/process.nix @@ -0,0 +1,48 @@ +{ config, lib, pkgs, options }: + +with lib; + +let + cfg = config.services.prometheus.exporters.process; + configFile = pkgs.writeText "process-exporter.yaml" (builtins.toJSON cfg.settings); +in +{ + port = 9256; + extraOpts = { + settings.process_names = mkOption { + type = types.listOf types.anything; + default = {}; + example = literalExample '' + { + process_names = [ + # Remove nix store path from process name + { name = "{{.Matches.Wrapped}} {{ .Matches.Args }}"; cmdline = [ "^/nix/store[^ ]*/(?P<Wrapped>[^ /]*) (?P<Args>.*)" ]; } + ]; + } + ''; + description = '' + All settings expressed as an Nix attrset. + + Check the official documentation for the corresponding YAML + settings that can all be used here: <link xlink:href="https://github.com/ncabatoff/process-exporter" /> + ''; + }; + }; + serviceOpts = { + serviceConfig = { + DynamicUser = false; + ExecStart = '' + ${pkgs.prometheus-process-exporter}/bin/process-exporter \ + --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \ + --config.path ${configFile} \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + NoNewPrivileges = true; + ProtectHome = true; + ProtectSystem = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + }; + }; +} diff --git a/nixos/modules/services/monitoring/prometheus/exporters/py-air-control.nix b/nixos/modules/services/monitoring/prometheus/exporters/py-air-control.nix new file mode 100644 index 00000000000..d9ab99221d9 --- /dev/null +++ b/nixos/modules/services/monitoring/prometheus/exporters/py-air-control.nix @@ -0,0 +1,53 @@ +{ config, lib, pkgs, options }: + +with lib; + +let + cfg = config.services.prometheus.exporters.py-air-control; + + workingDir = "/var/lib/${cfg.stateDir}"; + +in +{ + port = 9896; + extraOpts = { + deviceHostname = mkOption { + type = types.str; + example = "192.168.1.123"; + description = '' + The hostname of the air purification device from which to scrape the metrics. + ''; + }; + protocol = mkOption { + type = types.str; + default = "http"; + description = '' + The protocol to use when communicating with the air purification device. + Available: [http, coap, plain_coap] + ''; + }; + stateDir = mkOption { + type = types.str; + default = "prometheus-py-air-control-exporter"; + description = '' + Directory below <literal>/var/lib</literal> to store runtime data. + This directory will be created automatically using systemd's StateDirectory mechanism. + ''; + }; + }; + serviceOpts = { + serviceConfig = { + DynamicUser = false; + StateDirectory = cfg.stateDir; + WorkingDirectory = workingDir; + ExecStart = '' + ${pkgs.python3Packages.py-air-control-exporter}/bin/py-air-control-exporter \ + --host ${cfg.deviceHostname} \ + --protocol ${cfg.protocol} \ + --listen-port ${toString cfg.port} \ + --listen-address ${cfg.listenAddress} + ''; + Environment = [ "HOME=${workingDir}" ]; + }; + }; +} diff --git a/nixos/modules/services/monitoring/prometheus/exporters/rspamd.nix b/nixos/modules/services/monitoring/prometheus/exporters/rspamd.nix index 1f02ae20724..994670a376e 100644 --- a/nixos/modules/services/monitoring/prometheus/exporters/rspamd.nix +++ b/nixos/modules/services/monitoring/prometheus/exporters/rspamd.nix @@ -5,69 +5,58 @@ with lib; let cfg = config.services.prometheus.exporters.rspamd; - prettyJSON = conf: - pkgs.runCommand "rspamd-exporter-config.yml" { } '' - echo '${builtins.toJSON conf}' | ${pkgs.buildPackages.jq}/bin/jq '.' > $out - ''; + mkFile = conf: + pkgs.writeText "rspamd-exporter-config.yml" (builtins.toJSON conf); - generateConfig = extraLabels: (map (path: { - name = "rspamd_${replaceStrings [ "." " " ] [ "_" "_" ] path}"; - path = "$.${path}"; - labels = extraLabels; - }) [ - "actions.'add header'" - "actions.'no action'" - "actions.'rewrite subject'" - "actions.'soft reject'" - "actions.greylist" - "actions.reject" - "bytes_allocated" - "chunks_allocated" - "chunks_freed" - "chunks_oversized" - "connections" - "control_connections" - "ham_count" - "learned" - "pools_allocated" - "pools_freed" - "read_only" - "scanned" - "shared_chunks_allocated" - "spam_count" - "total_learns" - ]) ++ [{ - name = "rspamd_statfiles"; - type = "object"; - path = "$.statfiles[*]"; - labels = recursiveUpdate { - symbol = "$.symbol"; - type = "$.type"; - } extraLabels; - values = { - revision = "$.revision"; - size = "$.size"; - total = "$.total"; - used = "$.used"; - languages = "$.languages"; - users = "$.users"; - }; - }]; + generateConfig = extraLabels: { + metrics = (map (path: { + name = "rspamd_${replaceStrings [ "[" "." " " "]" "\\" "'" ] [ "_" "_" "_" "" "" "" ] path}"; + path = "{ .${path} }"; + labels = extraLabels; + }) [ + "actions['add\\ header']" + "actions['no\\ action']" + "actions['rewrite\\ subject']" + "actions['soft\\ reject']" + "actions.greylist" + "actions.reject" + "bytes_allocated" + "chunks_allocated" + "chunks_freed" + "chunks_oversized" + "connections" + "control_connections" + "ham_count" + "learned" + "pools_allocated" + "pools_freed" + "read_only" + "scanned" + "shared_chunks_allocated" + "spam_count" + "total_learns" + ]) ++ [{ + name = "rspamd_statfiles"; + type = "object"; + path = "{.statfiles[*]}"; + labels = recursiveUpdate { + symbol = "{.symbol}"; + type = "{.type}"; + } extraLabels; + values = { + revision = "{.revision}"; + size = "{.size}"; + total = "{.total}"; + used = "{.used}"; + languages = "{.languages}"; + users = "{.users}"; + }; + }]; + }; in { port = 7980; extraOpts = { - listenAddress = {}; # not used - - url = mkOption { - type = types.str; - description = '' - URL to the rspamd metrics endpoint. - Defaults to http://localhost:11334/stat when - <option>services.rspamd.enable</option> is true. - ''; - }; - extraLabels = mkOption { type = types.attrsOf types.str; default = { @@ -84,9 +73,25 @@ in }; }; serviceOpts.serviceConfig.ExecStart = '' - ${pkgs.prometheus-json-exporter}/bin/prometheus-json-exporter \ - --port ${toString cfg.port} \ - ${cfg.url} ${prettyJSON (generateConfig cfg.extraLabels)} \ + ${pkgs.prometheus-json-exporter}/bin/json_exporter \ + --config.file ${mkFile (generateConfig cfg.extraLabels)} \ + --web.listen-address "${cfg.listenAddress}:${toString cfg.port}" \ ${concatStringsSep " \\\n " cfg.extraFlags} ''; + + imports = [ + (mkRemovedOptionModule [ "url" ] '' + This option was removed. The URL of the rspamd metrics endpoint + must now be provided to the exporter by prometheus via the url + parameter `target'. + + In prometheus a scrape URL would look like this: + + http://some.rspamd-exporter.host:7980/probe?target=http://some.rspamd.host:11334/stat + + For more information, take a look at the official documentation + (https://github.com/prometheus-community/json_exporter) of the json_exporter. + '') + ({ options.warnings = options.warnings; options.assertions = options.assertions; }) + ]; } diff --git a/nixos/modules/services/monitoring/prometheus/exporters/rtl_433.nix b/nixos/modules/services/monitoring/prometheus/exporters/rtl_433.nix new file mode 100644 index 00000000000..01e420db389 --- /dev/null +++ b/nixos/modules/services/monitoring/prometheus/exporters/rtl_433.nix @@ -0,0 +1,78 @@ +{ config, lib, pkgs, options }: + +let + cfg = config.services.prometheus.exporters.rtl_433; +in +{ + port = 9550; + + extraOpts = let + mkMatcherOptionType = field: description: with lib.types; + listOf (submodule { + options = { + name = lib.mkOption { + type = str; + description = "Name to match."; + }; + "${field}" = lib.mkOption { + type = int; + inherit description; + }; + location = lib.mkOption { + type = str; + description = "Location to match."; + }; + }; + }); + in + { + rtl433Flags = lib.mkOption { + type = lib.types.str; + default = "-C si"; + example = "-C si -R 19"; + description = '' + Flags passed verbatim to rtl_433 binary. + Having <literal>-C si</literal> (the default) is recommended since only Celsius temperatures are parsed. + ''; + }; + channels = lib.mkOption { + type = mkMatcherOptionType "channel" "Channel to match."; + default = []; + example = [ + { name = "Acurite"; channel = 6543; location = "Kitchen"; } + ]; + description = '' + List of channel matchers to export. + ''; + }; + ids = lib.mkOption { + type = mkMatcherOptionType "id" "ID to match."; + default = []; + example = [ + { name = "Nexus"; id = 1; location = "Bedroom"; } + ]; + description = '' + List of ID matchers to export. + ''; + }; + }; + + serviceOpts = { + serviceConfig = { + # rtl-sdr udev rules make supported USB devices +rw by plugdev. + SupplementaryGroups = "plugdev"; + ExecStart = let + matchers = (map (m: + "--channel_matcher '${m.name},${toString m.channel},${m.location}'" + ) cfg.channels) ++ (map (m: + "--id_matcher '${m.name},${toString m.id},${m.location}'" + ) cfg.ids); in '' + ${pkgs.prometheus-rtl_433-exporter}/bin/rtl_433_prometheus \ + -listen ${cfg.listenAddress}:${toString cfg.port} \ + -subprocess "${pkgs.rtl_433}/bin/rtl_433 -F json ${cfg.rtl433Flags}" \ + ${lib.concatStringsSep " \\\n " matchers} \ + ${lib.concatStringsSep " \\\n " cfg.extraFlags} + ''; + }; + }; +} diff --git a/nixos/modules/services/monitoring/prometheus/exporters/script.nix b/nixos/modules/services/monitoring/prometheus/exporters/script.nix new file mode 100644 index 00000000000..104ab859f2e --- /dev/null +++ b/nixos/modules/services/monitoring/prometheus/exporters/script.nix @@ -0,0 +1,64 @@ +{ config, lib, pkgs, options }: + +with lib; + +let + cfg = config.services.prometheus.exporters.script; + configFile = pkgs.writeText "script-exporter.yaml" (builtins.toJSON cfg.settings); +in +{ + port = 9172; + extraOpts = { + settings.scripts = mkOption { + type = with types; listOf (submodule { + options = { + name = mkOption { + type = str; + example = "sleep"; + description = "Name of the script."; + }; + script = mkOption { + type = str; + example = "sleep 5"; + description = "Shell script to execute when metrics are requested."; + }; + timeout = mkOption { + type = nullOr int; + default = null; + example = 60; + description = "Optional timeout for the script in seconds."; + }; + }; + }); + example = literalExample '' + { + scripts = [ + { name = "sleep"; script = "sleep 5"; } + ]; + } + ''; + description = '' + All settings expressed as an Nix attrset. + + Check the official documentation for the corresponding YAML + settings that can all be used here: <link xlink:href="https://github.com/adhocteam/script_exporter#sample-configuration" /> + ''; + }; + }; + serviceOpts = { + serviceConfig = { + ExecStart = '' + ${pkgs.prometheus-script-exporter}/bin/script_exporter \ + --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \ + --config.file ${configFile} \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + NoNewPrivileges = true; + ProtectHome = true; + ProtectSystem = "strict"; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + }; + }; +} diff --git a/nixos/modules/services/monitoring/prometheus/exporters/smokeping.nix b/nixos/modules/services/monitoring/prometheus/exporters/smokeping.nix new file mode 100644 index 00000000000..0a7bb9c27be --- /dev/null +++ b/nixos/modules/services/monitoring/prometheus/exporters/smokeping.nix @@ -0,0 +1,60 @@ +{ config, lib, pkgs, options }: + +with lib; + +let + cfg = config.services.prometheus.exporters.smokeping; + goDuration = types.mkOptionType { + name = "goDuration"; + description = "Go duration (https://golang.org/pkg/time/#ParseDuration)"; + check = x: types.str.check x && builtins.match "(-?[0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+" x != null; + inherit (types.str) merge; + }; +in +{ + port = 9374; + extraOpts = { + telemetryPath = mkOption { + type = types.str; + default = "/metrics"; + description = '' + Path under which to expose metrics. + ''; + }; + pingInterval = mkOption { + type = goDuration; + default = "1s"; + description = '' + Interval between pings. + ''; + }; + buckets = mkOption { + type = types.commas; + default = "5e-05,0.0001,0.0002,0.0004,0.0008,0.0016,0.0032,0.0064,0.0128,0.0256,0.0512,0.1024,0.2048,0.4096,0.8192,1.6384,3.2768,6.5536,13.1072,26.2144"; + description = '' + List of buckets to use for the response duration histogram. + ''; + }; + hosts = mkOption { + type = with types; listOf str; + description = '' + List of endpoints to probe. + ''; + }; + }; + serviceOpts = { + serviceConfig = { + AmbientCapabilities = [ "CAP_NET_RAW" ]; + ExecStart = '' + ${pkgs.prometheus-smokeping-prober}/bin/smokeping_prober \ + --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \ + --web.telemetry-path ${cfg.telemetryPath} \ + --buckets ${cfg.buckets} \ + --ping.interval ${cfg.pingInterval} \ + --privileged \ + ${concatStringsSep " \\\n " cfg.extraFlags} \ + ${concatStringsSep " " cfg.hosts} + ''; + }; + }; +} diff --git a/nixos/modules/services/monitoring/prometheus/exporters/sql.nix b/nixos/modules/services/monitoring/prometheus/exporters/sql.nix new file mode 100644 index 00000000000..d9be724ebc0 --- /dev/null +++ b/nixos/modules/services/monitoring/prometheus/exporters/sql.nix @@ -0,0 +1,104 @@ +{ config, lib, pkgs, options }: +with lib; +let + cfg = config.services.prometheus.exporters.sql; + cfgOptions = { + options = with types; { + jobs = mkOption { + type = attrsOf (submodule jobOptions); + default = { }; + description = "An attrset of metrics scraping jobs to run."; + }; + }; + }; + jobOptions = { + options = with types; { + interval = mkOption { + type = str; + description = '' + How often to run this job, specified in + <link xlink:href="https://golang.org/pkg/time/#ParseDuration">Go duration</link> format. + ''; + }; + connections = mkOption { + type = listOf str; + description = "A list of connection strings of the SQL servers to scrape metrics from"; + }; + startupSql = mkOption { + type = listOf str; + default = []; + description = "A list of SQL statements to execute once after making a connection."; + }; + queries = mkOption { + type = attrsOf (submodule queryOptions); + description = "SQL queries to run."; + }; + }; + }; + queryOptions = { + options = with types; { + help = mkOption { + type = nullOr str; + default = null; + description = "A human-readable description of this metric."; + }; + labels = mkOption { + type = listOf str; + default = [ ]; + description = "A set of columns that will be used as Prometheus labels."; + }; + query = mkOption { + type = str; + description = "The SQL query to run."; + }; + values = mkOption { + type = listOf str; + description = "A set of columns that will be used as values of this metric."; + }; + }; + }; + + configFile = + if cfg.configFile != null + then cfg.configFile + else + let + nameInline = mapAttrsToList (k: v: v // { name = k; }); + renameStartupSql = j: removeAttrs (j // { startup_sql = j.startupSql; }) [ "startupSql" ]; + configuration = { + jobs = map renameStartupSql + (nameInline (mapAttrs (k: v: (v // { queries = nameInline v.queries; })) cfg.configuration.jobs)); + }; + in + builtins.toFile "config.yaml" (builtins.toJSON configuration); +in +{ + extraOpts = { + configFile = mkOption { + type = with types; nullOr path; + default = null; + description = '' + Path to configuration file. + ''; + }; + configuration = mkOption { + type = with types; nullOr (submodule cfgOptions); + default = null; + description = '' + Exporter configuration as nix attribute set. Mutually exclusive with 'configFile' option. + ''; + }; + }; + + port = 9237; + serviceOpts = { + serviceConfig = { + ExecStart = '' + ${pkgs.prometheus-sql-exporter}/bin/sql_exporter \ + -web.listen-address ${cfg.listenAddress}:${toString cfg.port} \ + -config.file ${configFile} \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + }; + }; +} diff --git a/nixos/modules/services/monitoring/prometheus/exporters/systemd.nix b/nixos/modules/services/monitoring/prometheus/exporters/systemd.nix new file mode 100644 index 00000000000..0514469b8a6 --- /dev/null +++ b/nixos/modules/services/monitoring/prometheus/exporters/systemd.nix @@ -0,0 +1,18 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let cfg = config.services.prometheus.exporters.systemd; + +in { + port = 9558; + + serviceOpts = { + serviceConfig = { + ExecStart = '' + ${pkgs.prometheus-systemd-exporter}/bin/systemd_exporter \ + --web.listen-address ${cfg.listenAddress}:${toString cfg.port} + ''; + }; + }; +} diff --git a/nixos/modules/services/monitoring/prometheus/exporters/unbound.nix b/nixos/modules/services/monitoring/prometheus/exporters/unbound.nix new file mode 100644 index 00000000000..56a559531c1 --- /dev/null +++ b/nixos/modules/services/monitoring/prometheus/exporters/unbound.nix @@ -0,0 +1,59 @@ +{ config, lib, pkgs, options }: + +with lib; + +let + cfg = config.services.prometheus.exporters.unbound; +in +{ + port = 9167; + extraOpts = { + fetchType = mkOption { + # TODO: add shm when upstream implemented it + type = types.enum [ "tcp" "uds" ]; + default = "uds"; + description = '' + Which methods the exporter uses to get the information from unbound. + ''; + }; + + telemetryPath = mkOption { + type = types.str; + default = "/metrics"; + description = '' + Path under which to expose metrics. + ''; + }; + + controlInterface = mkOption { + type = types.nullOr types.str; + default = null; + example = "/run/unbound/unbound.socket"; + description = '' + Path to the unbound socket for uds mode or the control interface port for tcp mode. + + Example: + uds-mode: /run/unbound/unbound.socket + tcp-mode: 127.0.0.1:8953 + ''; + }; + }; + + serviceOpts = mkMerge ([{ + serviceConfig = { + ExecStart = '' + ${pkgs.prometheus-unbound-exporter}/bin/unbound-telemetry \ + ${cfg.fetchType} \ + --bind ${cfg.listenAddress}:${toString cfg.port} \ + --path ${cfg.telemetryPath} \ + ${optionalString (cfg.controlInterface != null) "--control-interface ${cfg.controlInterface}"} \ + ${toString cfg.extraFlags} + ''; + }; + }] ++ [ + (mkIf config.services.unbound.enable { + after = [ "unbound.service" ]; + requires = [ "unbound.service" ]; + }) + ]); +} diff --git a/nixos/modules/services/monitoring/prometheus/exporters/unifi-poller.nix b/nixos/modules/services/monitoring/prometheus/exporters/unifi-poller.nix new file mode 100644 index 00000000000..394e6e201f0 --- /dev/null +++ b/nixos/modules/services/monitoring/prometheus/exporters/unifi-poller.nix @@ -0,0 +1,34 @@ +{ config, lib, pkgs, options }: + +with lib; + +let + cfg = config.services.prometheus.exporters.unifi-poller; + + configFile = pkgs.writeText "prometheus-unifi-poller-exporter.json" (generators.toJSON {} { + poller = { inherit (cfg.log) debug quiet; }; + unifi = { inherit (cfg) controllers; }; + influxdb.disable = true; + prometheus = { + http_listen = "${cfg.listenAddress}:${toString cfg.port}"; + report_errors = cfg.log.prometheusErrors; + }; + }); + +in { + port = 9130; + + extraOpts = { + inherit (options.services.unifi-poller.unifi) controllers; + log = { + debug = mkEnableOption "debug logging including line numbers, high resolution timestamps, per-device logs."; + quiet = mkEnableOption "startup and error logs only."; + prometheusErrors = mkEnableOption "emitting errors to prometheus."; + }; + }; + + serviceOpts.serviceConfig = { + ExecStart = "${pkgs.unifi-poller}/bin/unifi-poller --config ${configFile}"; + DynamicUser = false; + }; +} diff --git a/nixos/modules/services/monitoring/prometheus/xmpp-alerts.nix b/nixos/modules/services/monitoring/prometheus/xmpp-alerts.nix index 44b15cb2034..980c93c9c47 100644 --- a/nixos/modules/services/monitoring/prometheus/xmpp-alerts.nix +++ b/nixos/modules/services/monitoring/prometheus/xmpp-alerts.nix @@ -4,21 +4,29 @@ with lib; let cfg = config.services.prometheus.xmpp-alerts; - - configFile = pkgs.writeText "prometheus-xmpp-alerts.yml" (builtins.toJSON cfg.configuration); - + settingsFormat = pkgs.formats.yaml {}; + configFile = settingsFormat.generate "prometheus-xmpp-alerts.yml" cfg.settings; in - { - options.services.prometheus.xmpp-alerts = { + imports = [ + (mkRenamedOptionModule + [ "services" "prometheus" "xmpp-alerts" "configuration" ] + [ "services" "prometheus" "xmpp-alerts" "settings" ]) + ]; + options.services.prometheus.xmpp-alerts = { enable = mkEnableOption "XMPP Web hook service for Alertmanager"; - configuration = mkOption { - type = types.attrs; - description = "Configuration as attribute set which will be converted to YAML"; - }; + settings = mkOption { + type = settingsFormat.type; + default = {}; + description = '' + Configuration for prometheus xmpp-alerts, see + <link xlink:href="https://github.com/jelmer/prometheus-xmpp-alerts/blob/master/xmpp-alerts.yml.example"/> + for supported values. + ''; + }; }; config = mkIf cfg.enable { diff --git a/nixos/modules/services/monitoring/scollector.nix b/nixos/modules/services/monitoring/scollector.nix index 6f13ce889cb..ef535585e9b 100644 --- a/nixos/modules/services/monitoring/scollector.nix +++ b/nixos/modules/services/monitoring/scollector.nix @@ -113,7 +113,7 @@ in { description = "scollector metrics collector (part of Bosun)"; wantedBy = [ "multi-user.target" ]; - path = [ pkgs.coreutils pkgs.iproute ]; + path = [ pkgs.coreutils pkgs.iproute2 ]; serviceConfig = { User = cfg.user; diff --git a/nixos/modules/services/monitoring/smartd.nix b/nixos/modules/services/monitoring/smartd.nix index c72b4abfcdc..3ea25437114 100644 --- a/nixos/modules/services/monitoring/smartd.nix +++ b/nixos/modules/services/monitoring/smartd.nix @@ -36,7 +36,7 @@ let $SMARTD_MESSAGE EOF - } | ${pkgs.utillinux}/bin/wall 2>/dev/null + } | ${pkgs.util-linux}/bin/wall 2>/dev/null ''} ${optionalString nx.enable '' export DISPLAY=${nx.display} diff --git a/nixos/modules/services/monitoring/teamviewer.nix b/nixos/modules/services/monitoring/teamviewer.nix index 8d781d82d08..ce9e57a187c 100644 --- a/nixos/modules/services/monitoring/teamviewer.nix +++ b/nixos/modules/services/monitoring/teamviewer.nix @@ -31,14 +31,14 @@ in after = [ "NetworkManager-wait-online.service" "network.target" ]; preStart = "mkdir -pv /var/lib/teamviewer /var/log/teamviewer"; + startLimitIntervalSec = 60; + startLimitBurst = 10; serviceConfig = { Type = "forking"; ExecStart = "${pkgs.teamviewer}/bin/teamviewerd -d"; PIDFile = "/run/teamviewerd.pid"; ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; Restart = "on-abort"; - StartLimitInterval = "60"; - StartLimitBurst = "10"; }; }; }; diff --git a/nixos/modules/services/monitoring/telegraf.nix b/nixos/modules/services/monitoring/telegraf.nix index 5d131557e8b..4046260c164 100644 --- a/nixos/modules/services/monitoring/telegraf.nix +++ b/nixos/modules/services/monitoring/telegraf.nix @@ -5,14 +5,8 @@ with lib; let cfg = config.services.telegraf; - configFile = pkgs.runCommand "config.toml" { - buildInputs = [ pkgs.remarshal ]; - preferLocalBuild = true; - } '' - remarshal -if json -of toml \ - < ${pkgs.writeText "config.json" (builtins.toJSON cfg.extraConfig)} \ - > $out - ''; + settingsFormat = pkgs.formats.toml {}; + configFile = settingsFormat.generate "config.toml" cfg.extraConfig; in { ###### interface options = { @@ -26,22 +20,30 @@ in { type = types.package; }; + environmentFiles = mkOption { + type = types.listOf types.path; + default = []; + example = "/run/keys/telegraf.env"; + description = '' + File to load as environment file. Environment variables from this file + will be interpolated into the config file using envsubst with this + syntax: <literal>$ENVIRONMENT</literal> or <literal>''${VARIABLE}</literal>. + This is useful to avoid putting secrets into the nix store. + ''; + }; + extraConfig = mkOption { default = {}; description = "Extra configuration options for telegraf"; - type = types.attrs; + type = settingsFormat.type; example = { - outputs = { - influxdb = { - urls = ["http://localhost:8086"]; - database = "telegraf"; - }; + outputs.influxdb = { + urls = ["http://localhost:8086"]; + database = "telegraf"; }; - inputs = { - statsd = { - service_address = ":8125"; - delete_timings = true; - }; + inputs.statsd = { + service_address = ":8125"; + delete_timings = true; }; }; }; @@ -51,21 +53,38 @@ in { ###### implementation config = mkIf config.services.telegraf.enable { - systemd.services.telegraf = { + systemd.services.telegraf = let + finalConfigFile = if config.services.telegraf.environmentFiles == [] + then configFile + else "/var/run/telegraf/config.toml"; + in { description = "Telegraf Agent"; wantedBy = [ "multi-user.target" ]; after = [ "network-online.target" ]; serviceConfig = { - ExecStart=''${cfg.package}/bin/telegraf -config "${configFile}"''; + EnvironmentFile = config.services.telegraf.environmentFiles; + ExecStartPre = lib.optional (config.services.telegraf.environmentFiles != []) + (pkgs.writeShellScript "pre-start" '' + umask 077 + ${pkgs.envsubst}/bin/envsubst -i "${configFile}" > /var/run/telegraf/config.toml + ''); + ExecStart="${cfg.package}/bin/telegraf -config ${finalConfigFile}"; ExecReload="${pkgs.coreutils}/bin/kill -HUP $MAINPID"; + RuntimeDirectory = "telegraf"; User = "telegraf"; + Group = "telegraf"; Restart = "on-failure"; + # for ping probes + AmbientCapabilities = [ "CAP_NET_RAW" ]; }; }; users.users.telegraf = { uid = config.ids.uids.telegraf; + group = "telegraf"; description = "telegraf daemon user"; }; + + users.groups.telegraf = {}; }; } diff --git a/nixos/modules/services/monitoring/thanos.nix b/nixos/modules/services/monitoring/thanos.nix index 52dab28cf72..474ea4b2505 100644 --- a/nixos/modules/services/monitoring/thanos.nix +++ b/nixos/modules/services/monitoring/thanos.nix @@ -12,7 +12,7 @@ let }; optionToArgs = opt: v : optional (v != null) ''--${opt}="${toString v}"''; - flagToArgs = opt: v : optional v ''--${opt}''; + flagToArgs = opt: v : optional v "--${opt}"; listToArgs = opt: vs : map (v: ''--${opt}="${v}"'') vs; attrsToArgs = opt: kvs: mapAttrsToList (k: v: ''--${opt}=${k}=\"${v}\"'') kvs; @@ -67,7 +67,7 @@ let preferLocalBuild = true; json = builtins.toFile "${name}.json" (builtins.toJSON attrs); nativeBuildInputs = [ pkgs.remarshal ]; - } ''json2yaml -i $json -o $out''; + } "json2yaml -i $json -o $out"; thanos = cmd: "${cfg.package}/bin/thanos ${cmd}" + (let args = cfg.${cmd}.arguments; diff --git a/nixos/modules/services/monitoring/tuptime.nix b/nixos/modules/services/monitoring/tuptime.nix index 8f79d916599..17c5c1f56ea 100644 --- a/nixos/modules/services/monitoring/tuptime.nix +++ b/nixos/modules/services/monitoring/tuptime.nix @@ -34,7 +34,10 @@ in { users = { groups._tuptime.members = [ "_tuptime" ]; - users._tuptime.description = "tuptime database owner"; + users._tuptime = { + isSystemUser = true; + description = "tuptime database owner"; + }; }; systemd = { diff --git a/nixos/modules/services/monitoring/unifi-poller.nix b/nixos/modules/services/monitoring/unifi-poller.nix new file mode 100644 index 00000000000..208f5e4875b --- /dev/null +++ b/nixos/modules/services/monitoring/unifi-poller.nix @@ -0,0 +1,242 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.unifi-poller; + + configFile = pkgs.writeText "unifi-poller.json" (generators.toJSON {} { + inherit (cfg) poller influxdb prometheus unifi; + }); + +in { + options.services.unifi-poller = { + enable = mkEnableOption "unifi-poller"; + + poller = { + debug = mkOption { + type = types.bool; + default = false; + description = '' + Turns on line numbers, microsecond logging, and a per-device log. + This may be noisy if you have a lot of devices. It adds one line per device. + ''; + }; + quiet = mkOption { + type = types.bool; + default = false; + description = '' + Turns off per-interval logs. Only startup and error logs will be emitted. + ''; + }; + plugins = mkOption { + type = with types; listOf str; + default = []; + description = '' + Load additional plugins. + ''; + }; + }; + + prometheus = { + disable = mkOption { + type = types.bool; + default = false; + description = '' + Whether to disable the prometheus ouput plugin. + ''; + }; + http_listen = mkOption { + type = types.str; + default = "[::]:9130"; + description = '' + Bind the prometheus exporter to this IP or hostname. + ''; + }; + report_errors = mkOption { + type = types.bool; + default = false; + description = '' + Whether to report errors. + ''; + }; + }; + + influxdb = { + disable = mkOption { + type = types.bool; + default = false; + description = '' + Whether to disable the influxdb ouput plugin. + ''; + }; + url = mkOption { + type = types.str; + default = "http://127.0.0.1:8086"; + description = '' + URL of the influxdb host. + ''; + }; + user = mkOption { + type = types.str; + default = "unifipoller"; + description = '' + Username for the influxdb. + ''; + }; + pass = mkOption { + type = types.path; + default = pkgs.writeText "unifi-poller-influxdb-default.password" "unifipoller"; + defaultText = "unifi-poller-influxdb-default.password"; + description = '' + Path of a file containing the password for influxdb. + This file needs to be readable by the unifi-poller user. + ''; + apply = v: "file://${v}"; + }; + db = mkOption { + type = types.str; + default = "unifi"; + description = '' + Database name. Database should exist. + ''; + }; + verify_ssl = mkOption { + type = types.bool; + default = true; + description = '' + Verify the influxdb's certificate. + ''; + }; + interval = mkOption { + type = types.str; + default = "30s"; + description = '' + Setting this lower than the Unifi controller's refresh + interval may lead to zeroes in your database. + ''; + }; + }; + + unifi = let + controllerOptions = { + user = mkOption { + type = types.str; + default = "unifi"; + description = '' + Unifi service user name. + ''; + }; + pass = mkOption { + type = types.path; + default = pkgs.writeText "unifi-poller-unifi-default.password" "unifi"; + defaultText = "unifi-poller-unifi-default.password"; + description = '' + Path of a file containing the password for the unifi service user. + This file needs to be readable by the unifi-poller user. + ''; + apply = v: "file://${v}"; + }; + url = mkOption { + type = types.str; + default = "https://unifi:8443"; + description = '' + URL of the Unifi controller. + ''; + }; + sites = mkOption { + type = with types; either (enum [ "default" "all" ]) (listOf str); + default = "all"; + description = '' + List of site names for which statistics should be exported. + Or the string "default" for the default site or the string "all" for all sites. + ''; + apply = toList; + }; + save_ids = mkOption { + type = types.bool; + default = false; + description = '' + Collect and save data from the intrusion detection system to influxdb. + ''; + }; + save_dpi = mkOption { + type = types.bool; + default = false; + description = '' + Collect and save data from deep packet inspection. + Adds around 150 data points and impacts performance. + ''; + }; + save_sites = mkOption { + type = types.bool; + default = true; + description = '' + Collect and save site data. + ''; + }; + hash_pii = mkOption { + type = types.bool; + default = false; + description = '' + Hash, with md5, client names and MAC addresses. This attempts + to protect personally identifiable information. + ''; + }; + verify_ssl = mkOption { + type = types.bool; + default = true; + description = '' + Verify the Unifi controller's certificate. + ''; + }; + }; + + in { + dynamic = mkOption { + type = types.bool; + default = false; + description = '' + Let prometheus select which controller to poll when scraping. + Use with default credentials. See unifi-poller wiki for more. + ''; + }; + + defaults = controllerOptions; + + controllers = mkOption { + type = with types; listOf (submodule { options = controllerOptions; }); + default = []; + description = '' + List of Unifi controllers to poll. Use defaults if empty. + ''; + apply = map (flip removeAttrs [ "_module" ]); + }; + }; + }; + + config = mkIf cfg.enable { + users.groups.unifi-poller = { }; + users.users.unifi-poller = { + description = "unifi-poller Service User"; + group = "unifi-poller"; + isSystemUser = true; + }; + + systemd.services.unifi-poller = { + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + serviceConfig = { + ExecStart = "${pkgs.unifi-poller}/bin/unifi-poller --config ${configFile}"; + Restart = "always"; + PrivateTmp = true; + ProtectHome = true; + ProtectSystem = "full"; + DevicePolicy = "closed"; + NoNewPrivileges = true; + User = "unifi-poller"; + WorkingDirectory = "/tmp"; + }; + }; + }; +} diff --git a/nixos/modules/services/monitoring/ups.nix b/nixos/modules/services/monitoring/ups.nix index a45e806d4ad..ae5097c5442 100644 --- a/nixos/modules/services/monitoring/ups.nix +++ b/nixos/modules/services/monitoring/ups.nix @@ -205,7 +205,7 @@ in after = [ "upsd.service" ]; wantedBy = [ "multi-user.target" ]; # TODO: replace 'root' by another username. - script = ''${pkgs.nut}/bin/upsdrvctl -u root start''; + script = "${pkgs.nut}/bin/upsdrvctl -u root start"; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; diff --git a/nixos/modules/services/monitoring/vnstat.nix b/nixos/modules/services/monitoring/vnstat.nix index e9bedb704a4..5e19c399568 100644 --- a/nixos/modules/services/monitoring/vnstat.nix +++ b/nixos/modules/services/monitoring/vnstat.nix @@ -6,21 +6,21 @@ let cfg = config.services.vnstat; in { options.services.vnstat = { - enable = mkOption { - type = types.bool; - default = false; - description = '' - Whether to enable update of network usage statistics via vnstatd. - ''; - }; + enable = mkEnableOption "update of network usage statistics via vnstatd"; }; config = mkIf cfg.enable { - users.users.vnstatd = { - isSystemUser = true; - description = "vnstat daemon user"; - home = "/var/lib/vnstat"; - createHome = true; + + environment.systemPackages = [ pkgs.vnstat ]; + + users = { + groups.vnstatd = {}; + + users.vnstatd = { + isSystemUser = true; + group = "vnstatd"; + description = "vnstat daemon user"; + }; }; systemd.services.vnstat = { @@ -33,7 +33,6 @@ in { "man:vnstat(1)" "man:vnstat.conf(5)" ]; - preStart = "chmod 755 /var/lib/vnstat"; serviceConfig = { ExecStart = "${pkgs.vnstat}/bin/vnstatd -n"; ExecReload = "${pkgs.procps}/bin/kill -HUP $MAINPID"; @@ -52,7 +51,10 @@ in { RestrictNamespaces = true; User = "vnstatd"; + Group = "vnstatd"; }; }; }; + + meta.maintainers = [ maintainers.evils ]; } diff --git a/nixos/modules/services/monitoring/zabbix-agent.nix b/nixos/modules/services/monitoring/zabbix-agent.nix index 73eed7aa66a..7eb6449e384 100644 --- a/nixos/modules/services/monitoring/zabbix-agent.nix +++ b/nixos/modules/services/monitoring/zabbix-agent.nix @@ -128,11 +128,16 @@ in { LogType = "console"; Server = cfg.server; - ListenIP = cfg.listen.ip; ListenPort = cfg.listen.port; - LoadModule = builtins.attrNames cfg.modules; } - (mkIf (cfg.modules != {}) { LoadModulePath = "${moduleEnv}/lib"; }) + (mkIf (cfg.modules != {}) { + LoadModule = builtins.attrNames cfg.modules; + LoadModulePath = "${moduleEnv}/lib"; + }) + + # the default value for "ListenIP" is 0.0.0.0 but zabbix agent 2 cannot accept configuration files which + # explicitly set "ListenIP" to the default value... + (mkIf (cfg.listen.ip != "0.0.0.0") { ListenIP = cfg.listen.ip; }) ]; networking.firewall = mkIf cfg.openFirewall { @@ -152,7 +157,10 @@ in wantedBy = [ "multi-user.target" ]; - path = [ "/run/wrappers" ] ++ cfg.extraPackages; + # https://www.zabbix.com/documentation/current/manual/config/items/userparameters + # > User parameters are commands executed by Zabbix agent. + # > /bin/sh is used as a command line interpreter under UNIX operating systems. + path = with pkgs; [ bash "/run/wrappers" ] ++ cfg.extraPackages; serviceConfig = { ExecStart = "@${cfg.package}/sbin/zabbix_agentd zabbix_agentd -f --config ${configFile}"; |