grafana-alloy: don't build the frontend twice
[NixPkgs.git] / nixos / modules / services / monitoring / netdata.nix
blob0929b0ab5171cb1de278a679ddb42d333007d270
1 { config, pkgs, lib, ... }:
3 with lib;
5 let
6   cfg = config.services.netdata;
8   wrappedPlugins = pkgs.runCommand "wrapped-plugins" { preferLocalBuild = true; } ''
9     mkdir -p $out/libexec/netdata/plugins.d
10     ln -s /run/wrappers/bin/apps.plugin $out/libexec/netdata/plugins.d/apps.plugin
11     ln -s /run/wrappers/bin/cgroup-network $out/libexec/netdata/plugins.d/cgroup-network
12     ln -s /run/wrappers/bin/perf.plugin $out/libexec/netdata/plugins.d/perf.plugin
13     ln -s /run/wrappers/bin/slabinfo.plugin $out/libexec/netdata/plugins.d/slabinfo.plugin
14     ln -s /run/wrappers/bin/freeipmi.plugin $out/libexec/netdata/plugins.d/freeipmi.plugin
15     ln -s /run/wrappers/bin/systemd-journal.plugin $out/libexec/netdata/plugins.d/systemd-journal.plugin
16     ln -s /run/wrappers/bin/logs-management.plugin $out/libexec/netdata/plugins.d/logs-management.plugin
17     ln -s /run/wrappers/bin/network-viewer.plugin $out/libexec/netdata/plugins.d/network-viewer.plugin
18     ln -s /run/wrappers/bin/debugfs.plugin $out/libexec/netdata/plugins.d/debugfs.plugin
19   '';
21   plugins = [
22     "${cfg.package}/libexec/netdata/plugins.d"
23     "${wrappedPlugins}/libexec/netdata/plugins.d"
24   ] ++ cfg.extraPluginPaths;
26   configDirectory = pkgs.runCommand "netdata-config-d" { } ''
27     mkdir $out
28     ${concatStringsSep "\n" (mapAttrsToList (path: file: ''
29         mkdir -p "$out/$(dirname ${path})"
30         ln -s "${file}" "$out/${path}"
31       '') cfg.configDir)}
32   '';
34   localConfig = {
35     global = {
36       "config directory" = "/etc/netdata/conf.d";
37       "plugins directory" = concatStringsSep " " plugins;
38     };
39     web = {
40       "web files owner" = "root";
41       "web files group" = "root";
42     };
43     "plugin:cgroups" = {
44       "script to get cgroup network interfaces" = "${wrappedPlugins}/libexec/netdata/plugins.d/cgroup-network";
45       "use unified cgroups" = "yes";
46     };
47   };
48   mkConfig = generators.toINI {} (recursiveUpdate localConfig cfg.config);
49   configFile = pkgs.writeText "netdata.conf" (if cfg.configText != null then cfg.configText else mkConfig);
51   defaultUser = "netdata";
53   isThereAnyWireGuardTunnels = config.networking.wireguard.enable || lib.any (c: lib.hasAttrByPath [ "netdevConfig" "Kind" ] c && c.netdevConfig.Kind == "wireguard") (builtins.attrValues config.systemd.network.netdevs);
54 in {
55   options = {
56     services.netdata = {
57       enable = mkEnableOption "netdata";
59       package = mkPackageOption pkgs "netdata" { };
61       user = mkOption {
62         type = types.str;
63         default = "netdata";
64         description = "User account under which netdata runs.";
65       };
67       group = mkOption {
68         type = types.str;
69         default = "netdata";
70         description = "Group under which netdata runs.";
71       };
73       configText = mkOption {
74         type = types.nullOr types.lines;
75         description = "Verbatim netdata.conf, cannot be combined with config.";
76         default = null;
77         example = ''
78           [global]
79           debug log = syslog
80           access log = syslog
81           error log = syslog
82         '';
83       };
85       python = {
86         enable = mkOption {
87           type = types.bool;
88           default = true;
89           description = ''
90             Whether to enable python-based plugins
91           '';
92         };
93         recommendedPythonPackages = mkOption {
94           type = types.bool;
95           default = false;
96           description = ''
97             Whether to enable a set of recommended Python plugins
98             by installing extra Python packages.
99           '';
100         };
101         extraPackages = mkOption {
102           type = types.functionTo (types.listOf types.package);
103           default = ps: [];
104           defaultText = literalExpression "ps: []";
105           example = literalExpression ''
106             ps: [
107               ps.psycopg2
108               ps.docker
109               ps.dnspython
110             ]
111           '';
112           description = ''
113             Extra python packages available at runtime
114             to enable additional python plugins.
115           '';
116         };
117       };
119       extraPluginPaths = mkOption {
120         type = types.listOf types.path;
121         default = [ ];
122         example = literalExpression ''
123           [ "/path/to/plugins.d" ]
124         '';
125         description = ''
126           Extra paths to add to the netdata global "plugins directory"
127           option.  Useful for when you want to include your own
128           collection scripts.
130           Details about writing a custom netdata plugin are available at:
131           <https://docs.netdata.cloud/collectors/plugins.d/>
133           Cannot be combined with configText.
134         '';
135       };
137       config = mkOption {
138         type = types.attrsOf types.attrs;
139         default = {};
140         description = "netdata.conf configuration as nix attributes. cannot be combined with configText.";
141         example = literalExpression ''
142           global = {
143             "debug log" = "syslog";
144             "access log" = "syslog";
145             "error log" = "syslog";
146           };
147         '';
148       };
150       configDir = mkOption {
151         type = types.attrsOf types.path;
152         default = {};
153         description = ''
154           Complete netdata config directory except netdata.conf.
155           The default configuration is merged with changes
156           defined in this option.
157           Each top-level attribute denotes a path in the configuration
158           directory as in environment.etc.
159           Its value is the absolute path and must be readable by netdata.
160           Cannot be combined with configText.
161         '';
162         example = literalExpression ''
163           "health_alarm_notify.conf" = pkgs.writeText "health_alarm_notify.conf" '''
164             sendmail="/path/to/sendmail"
165           ''';
166           "health.d" = "/run/secrets/netdata/health.d";
167         '';
168       };
170       claimTokenFile = mkOption {
171         type = types.nullOr types.path;
172         default = null;
173         description = ''
174           If set, automatically registers the agent using the given claim token
175           file.
176         '';
177       };
179       enableAnalyticsReporting = mkOption {
180         type = types.bool;
181         default = false;
182         description = ''
183           Enable reporting of anonymous usage statistics to Netdata Inc. via either
184           Google Analytics (in versions prior to 1.29.4), or Netdata Inc.'s
185           self-hosted PostHog (in versions 1.29.4 and later).
186           See: <https://learn.netdata.cloud/docs/agent/anonymous-statistics>
187         '';
188       };
190       deadlineBeforeStopSec = mkOption {
191         type = types.int;
192         default = 120;
193         description = ''
194           In order to detect when netdata is misbehaving, we run a concurrent task pinging netdata (wait-for-netdata-up)
195           in the systemd unit.
197           If after a while, this task does not succeed, we stop the unit and mark it as failed.
199           You can control this deadline in seconds with this option, it's useful to bump it
200           if you have (1) a lot of data (2) doing upgrades (3) have low IOPS/throughput.
201         '';
202       };
203     };
204   };
206   config = mkIf cfg.enable {
207     assertions =
208       [ { assertion = cfg.config != {} -> cfg.configText == null ;
209           message = "Cannot specify both config and configText";
210         }
211       ];
213     # Includes a set of recommended Python plugins in exchange of imperfect disk consumption.
214     services.netdata.python.extraPackages = lib.mkIf cfg.python.recommendedPythonPackages (ps: [
215       ps.requests
216       ps.pandas
217       ps.numpy
218       ps.psycopg2
219       ps.python-ldap
220       ps.netdata-pandas
221       ps.changefinder
222     ]);
224     services.netdata.configDir.".opt-out-from-anonymous-statistics" = mkIf (!cfg.enableAnalyticsReporting) (pkgs.writeText ".opt-out-from-anonymous-statistics" "");
225     environment.etc."netdata/netdata.conf".source = configFile;
226     environment.etc."netdata/conf.d".source = configDirectory;
228     systemd.services.netdata = {
229       description = "Real time performance monitoring";
230       after = [ "network.target" "suid-sgid-wrappers.service" ];
231       # No wrapper means no "useful" netdata.
232       requires = [ "suid-sgid-wrappers.service" ];
233       wantedBy = [ "multi-user.target" ];
234       path = (with pkgs; [
235           curl
236           gawk
237           iproute2
238           which
239           procps
240           bash
241           nvme-cli # for go.d
242           iw # for charts.d
243           apcupsd # for charts.d
244           # TODO: firehol # for FireQoS -- this requires more NixOS module support.
245           util-linux # provides logger command; required for syslog health alarms
246       ])
247         ++ lib.optional cfg.python.enable (pkgs.python3.withPackages cfg.python.extraPackages)
248         ++ lib.optional config.virtualisation.libvirtd.enable config.virtualisation.libvirtd.package
249         ++ lib.optional config.virtualisation.docker.enable config.virtualisation.docker.package
250         ++ lib.optionals config.virtualisation.podman.enable [ pkgs.jq config.virtualisation.podman.package ]
251         ++ lib.optional config.boot.zfs.enabled config.boot.zfs.package;
252       environment = {
253         PYTHONPATH = "${cfg.package}/libexec/netdata/python.d/python_modules";
254         NETDATA_PIPENAME = "/run/netdata/ipc";
255       } // lib.optionalAttrs (!cfg.enableAnalyticsReporting) {
256         DO_NOT_TRACK = "1";
257       };
258       restartTriggers = [
259         config.environment.etc."netdata/netdata.conf".source
260         config.environment.etc."netdata/conf.d".source
261       ];
262       serviceConfig = {
263         ExecStart = "${cfg.package}/bin/netdata -P /run/netdata/netdata.pid -D -c /etc/netdata/netdata.conf";
264         ExecReload = "${pkgs.util-linux}/bin/kill -s HUP -s USR1 -s USR2 $MAINPID";
265         ExecStartPost = pkgs.writeShellScript "wait-for-netdata-up" ''
266           while [ "$(${cfg.package}/bin/netdatacli ping)" != pong ]; do sleep 0.5; done
267         '';
269         TimeoutStopSec = cfg.deadlineBeforeStopSec;
270         Restart = "on-failure";
271         # User and group
272         User = cfg.user;
273         Group = cfg.group;
274         # Performance
275         LimitNOFILE = "30000";
276         # Runtime directory and mode
277         RuntimeDirectory = "netdata";
278         RuntimeDirectoryMode = "0750";
279         # State directory and mode
280         StateDirectory = "netdata";
281         StateDirectoryMode = "0750";
282         # Cache directory and mode
283         CacheDirectory = "netdata";
284         CacheDirectoryMode = "0750";
285         # Logs directory and mode
286         LogsDirectory = "netdata";
287         LogsDirectoryMode = "0750";
288         # Configuration directory and mode
289         ConfigurationDirectory = "netdata";
290         ConfigurationDirectoryMode = "0755";
291         # AmbientCapabilities
292         AmbientCapabilities = lib.optional isThereAnyWireGuardTunnels "CAP_NET_ADMIN";
293         # Capabilities
294         CapabilityBoundingSet = [
295           "CAP_DAC_OVERRIDE"      # is required for freeipmi and slabinfo plugins
296           "CAP_DAC_READ_SEARCH"   # is required for apps and systemd-journal plugin
297           "CAP_FOWNER"            # is required for freeipmi plugin
298           "CAP_SETPCAP"           # is required for apps, perf and slabinfo plugins
299           "CAP_SYS_ADMIN"         # is required for perf plugin
300           "CAP_SYS_PTRACE"        # is required for apps plugin
301           "CAP_SYS_RESOURCE"      # is required for ebpf plugin
302           "CAP_NET_RAW"           # is required for fping app
303           "CAP_SYS_CHROOT"        # is required for cgroups plugin
304           "CAP_SETUID"            # is required for cgroups and cgroups-network plugins
305           "CAP_SYSLOG"            # is required for systemd-journal plugin
306         ] ++ lib.optional isThereAnyWireGuardTunnels "CAP_NET_ADMIN";
307         # Sandboxing
308         ProtectSystem = "full";
309         ProtectHome = "read-only";
310         PrivateTmp = true;
311         ProtectControlGroups = true;
312         PrivateMounts = true;
313       } // (lib.optionalAttrs (cfg.claimTokenFile != null) {
314         LoadCredential = [
315           "netdata_claim_token:${cfg.claimTokenFile}"
316         ];
318         ExecStartPre = pkgs.writeShellScript "netdata-claim" ''
319           set -euo pipefail
321           if [[ -f /var/lib/netdata/cloud.d/claimed_id ]]; then
322             # Already registered
323             exit
324           fi
326           exec ${cfg.package}/bin/netdata-claim.sh \
327             -token="$(< "$CREDENTIALS_DIRECTORY/netdata_claim_token")" \
328             -url=https://app.netdata.cloud \
329             -daemon-not-running
330         '';
331       });
332     };
334     systemd.enableCgroupAccounting = true;
336     security.wrappers = {
337       "apps.plugin" = {
338         source = "${cfg.package}/libexec/netdata/plugins.d/apps.plugin.org";
339         capabilities = "cap_dac_read_search,cap_sys_ptrace+ep";
340         owner = cfg.user;
341         group = cfg.group;
342         permissions = "u+rx,g+x,o-rwx";
343       };
345       "debugfs.plugin" = {
346         source = "${cfg.package}/libexec/netdata/plugins.d/debugfs.plugin.org";
347         capabilities = "cap_dac_read_search+ep";
348         owner = cfg.user;
349         group = cfg.group;
350         permissions = "u+rx,g+x,o-rwx";
351       };
353       "cgroup-network" = {
354         source = "${cfg.package}/libexec/netdata/plugins.d/cgroup-network.org";
355         capabilities = "cap_setuid+ep";
356         owner = cfg.user;
357         group = cfg.group;
358         permissions = "u+rx,g+x,o-rwx";
359       };
361       "perf.plugin" = {
362         source = "${cfg.package}/libexec/netdata/plugins.d/perf.plugin.org";
363         capabilities = "cap_sys_admin+ep";
364         owner = cfg.user;
365         group = cfg.group;
366         permissions = "u+rx,g+x,o-rwx";
367       };
369       "systemd-journal.plugin" = {
370         source = "${cfg.package}/libexec/netdata/plugins.d/systemd-journal.plugin.org";
371         capabilities = "cap_dac_read_search,cap_syslog+ep";
372         owner = cfg.user;
373         group = cfg.group;
374         permissions = "u+rx,g+x,o-rwx";
375       };
377       "slabinfo.plugin" = {
378         source = "${cfg.package}/libexec/netdata/plugins.d/slabinfo.plugin.org";
379         capabilities = "cap_dac_override+ep";
380         owner = cfg.user;
381         group = cfg.group;
382         permissions = "u+rx,g+x,o-rwx";
383       };
385     } // optionalAttrs (cfg.package.withIpmi) {
386       "freeipmi.plugin" = {
387         source = "${cfg.package}/libexec/netdata/plugins.d/freeipmi.plugin.org";
388         capabilities = "cap_dac_override,cap_fowner+ep";
389         owner = cfg.user;
390         group = cfg.group;
391         permissions = "u+rx,g+x,o-rwx";
392       };
393     } // optionalAttrs (cfg.package.withNetworkViewer) {
394       "network-viewer.plugin" = {
395         source = "${cfg.package}/libexec/netdata/plugins.d/network-viewer.plugin.org";
396         capabilities = "cap_sys_admin,cap_dac_read_search,cap_sys_ptrace+ep";
397         owner = cfg.user;
398         group = cfg.group;
399         permissions = "u+rx,g+x,o-rwx";
400       };
401     };
403     security.pam.loginLimits = [
404       { domain = "netdata"; type = "soft"; item = "nofile"; value = "10000"; }
405       { domain = "netdata"; type = "hard"; item = "nofile"; value = "30000"; }
406     ];
408     users.users = optionalAttrs (cfg.user == defaultUser) {
409       ${defaultUser} = {
410         group = defaultUser;
411         isSystemUser = true;
412         extraGroups = lib.optional config.virtualisation.docker.enable "docker"
413           ++ lib.optional config.virtualisation.podman.enable "podman";
414       };
415     };
417     users.groups = optionalAttrs (cfg.group == defaultUser) {
418       ${defaultUser} = { };
419     };
421   };