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