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