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