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