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