1{ options, config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.grafana;
7 opt = options.services.grafana;
8 provisioningSettingsFormat = pkgs.formats.yaml { };
9 declarativePlugins = pkgs.linkFarm "grafana-plugins" (builtins.map (pkg: { name = pkg.pname; path = pkg; }) cfg.declarativePlugins);
10 useMysql = cfg.settings.database.type == "mysql";
11 usePostgresql = cfg.settings.database.type == "postgres";
12
13 # Prefer using the values from the default config file[0] directly. This way,
14 # people reading the NixOS manual can see them without cross-referencing the
15 # official documentation.
16 #
17 # However, if there is no default entry or if the setting is optional, use
18 # `null` as the default value. It will be turned into the empty string.
19 #
20 # If a setting is a list, always allow setting it as a plain string as well.
21 #
22 # [0]: https://github.com/grafana/grafana/blob/main/conf/defaults.ini
23 settingsFormatIni = pkgs.formats.ini {
24 listToValue = concatMapStringsSep " " (generators.mkValueStringDefault { });
25 mkKeyValue = generators.mkKeyValueDefault
26 {
27 mkValueString = v:
28 if v == null then ""
29 else generators.mkValueStringDefault { } v;
30 }
31 "=";
32 };
33 configFile = settingsFormatIni.generate "config.ini" cfg.settings;
34
35 mkProvisionCfg = name: attr: provisionCfg:
36 if provisionCfg.path != null
37 then provisionCfg.path
38 else
39 provisioningSettingsFormat.generate "${name}.yaml"
40 (if provisionCfg.settings != null
41 then provisionCfg.settings
42 else {
43 apiVersion = 1;
44 ${attr} = [ ];
45 });
46
47 datasourceFileOrDir = mkProvisionCfg "datasource" "datasources" cfg.provision.datasources;
48 dashboardFileOrDir = mkProvisionCfg "dashboard" "providers" cfg.provision.dashboards;
49
50 notifierConfiguration = {
51 apiVersion = 1;
52 notifiers = cfg.provision.notifiers;
53 };
54
55 notifierFileOrDir = pkgs.writeText "notifier.yaml" (builtins.toJSON notifierConfiguration);
56
57 generateAlertingProvisioningYaml = x:
58 if (cfg.provision.alerting."${x}".path == null)
59 then provisioningSettingsFormat.generate "${x}.yaml" cfg.provision.alerting."${x}".settings
60 else cfg.provision.alerting."${x}".path;
61 rulesFileOrDir = generateAlertingProvisioningYaml "rules";
62 contactPointsFileOrDir = generateAlertingProvisioningYaml "contactPoints";
63 policiesFileOrDir = generateAlertingProvisioningYaml "policies";
64 templatesFileOrDir = generateAlertingProvisioningYaml "templates";
65 muteTimingsFileOrDir = generateAlertingProvisioningYaml "muteTimings";
66
67 ln = { src, dir, filename }: ''
68 if [[ -d "${src}" ]]; then
69 pushd $out/${dir} &>/dev/null
70 lndir "${src}"
71 popd &>/dev/null
72 else
73 ln -sf ${src} $out/${dir}/${filename}.yaml
74 fi
75 '';
76 provisionConfDir = pkgs.runCommand "grafana-provisioning" { nativeBuildInputs = [ pkgs.xorg.lndir ]; } ''
77 mkdir -p $out/{datasources,dashboards,notifiers,alerting}
78 ${ln { src = datasourceFileOrDir; dir = "datasources"; filename = "datasource"; }}
79 ${ln { src = dashboardFileOrDir; dir = "dashboards"; filename = "dashboard"; }}
80 ${ln { src = notifierFileOrDir; dir = "notifiers"; filename = "notifier"; }}
81 ${ln { src = rulesFileOrDir; dir = "alerting"; filename = "rules"; }}
82 ${ln { src = contactPointsFileOrDir; dir = "alerting"; filename = "contactPoints"; }}
83 ${ln { src = policiesFileOrDir; dir = "alerting"; filename = "policies"; }}
84 ${ln { src = templatesFileOrDir; dir = "alerting"; filename = "templates"; }}
85 ${ln { src = muteTimingsFileOrDir; dir = "alerting"; filename = "muteTimings"; }}
86 '';
87
88 # Get a submodule without any embedded metadata:
89 _filter = x: filterAttrs (k: v: k != "_module") x;
90
91 # https://grafana.com/docs/grafana/latest/administration/provisioning/#datasources
92 grafanaTypes.datasourceConfig = types.submodule {
93 freeformType = provisioningSettingsFormat.type;
94
95 options = {
96 name = mkOption {
97 type = types.str;
98 description = lib.mdDoc "Name of the datasource. Required.";
99 };
100 type = mkOption {
101 type = types.str;
102 description = lib.mdDoc "Datasource type. Required.";
103 };
104 access = mkOption {
105 type = types.enum [ "proxy" "direct" ];
106 default = "proxy";
107 description = lib.mdDoc "Access mode. proxy or direct (Server or Browser in the UI). Required.";
108 };
109 uid = mkOption {
110 type = types.nullOr types.str;
111 default = null;
112 description = lib.mdDoc "Custom UID which can be used to reference this datasource in other parts of the configuration, if not specified will be generated automatically.";
113 };
114 url = mkOption {
115 type = types.str;
116 default = "localhost";
117 description = lib.mdDoc "Url of the datasource.";
118 };
119 editable = mkOption {
120 type = types.bool;
121 default = false;
122 description = lib.mdDoc "Allow users to edit datasources from the UI.";
123 };
124 jsonData = mkOption {
125 type = types.nullOr types.attrs;
126 default = null;
127 description = lib.mdDoc "Extra data for datasource plugins.";
128 };
129 secureJsonData = mkOption {
130 type = types.nullOr types.attrs;
131 default = null;
132 description = lib.mdDoc ''
133 Datasource specific secure configuration. Please note that the contents of this option
134 will end up in a world-readable Nix store. Use the file provider
135 pointing at a reasonably secured file in the local filesystem
136 to work around that. Look at the documentation for details:
137 <https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider>
138 '';
139 };
140 };
141 };
142
143 # https://grafana.com/docs/grafana/latest/administration/provisioning/#dashboards
144 grafanaTypes.dashboardConfig = types.submodule {
145 freeformType = provisioningSettingsFormat.type;
146
147 options = {
148 name = mkOption {
149 type = types.str;
150 default = "default";
151 description = lib.mdDoc "A unique provider name.";
152 };
153 type = mkOption {
154 type = types.str;
155 default = "file";
156 description = lib.mdDoc "Dashboard provider type.";
157 };
158 options.path = mkOption {
159 type = types.path;
160 description = lib.mdDoc "Path grafana will watch for dashboards. Required when using the 'file' type.";
161 };
162 };
163 };
164
165 grafanaTypes.notifierConfig = types.submodule {
166 options = {
167 name = mkOption {
168 type = types.str;
169 default = "default";
170 description = lib.mdDoc "Notifier name.";
171 };
172 type = mkOption {
173 type = types.enum [ "dingding" "discord" "email" "googlechat" "hipchat" "kafka" "line" "teams" "opsgenie" "pagerduty" "prometheus-alertmanager" "pushover" "sensu" "sensugo" "slack" "telegram" "threema" "victorops" "webhook" ];
174 description = lib.mdDoc "Notifier type.";
175 };
176 uid = mkOption {
177 type = types.str;
178 description = lib.mdDoc "Unique notifier identifier.";
179 };
180 org_id = mkOption {
181 type = types.int;
182 default = 1;
183 description = lib.mdDoc "Organization ID.";
184 };
185 org_name = mkOption {
186 type = types.str;
187 default = "Main Org.";
188 description = lib.mdDoc "Organization name.";
189 };
190 is_default = mkOption {
191 type = types.bool;
192 description = lib.mdDoc "Is the default notifier.";
193 default = false;
194 };
195 send_reminder = mkOption {
196 type = types.bool;
197 default = true;
198 description = lib.mdDoc "Should the notifier be sent reminder notifications while alerts continue to fire.";
199 };
200 frequency = mkOption {
201 type = types.str;
202 default = "5m";
203 description = lib.mdDoc "How frequently should the notifier be sent reminders.";
204 };
205 disable_resolve_message = mkOption {
206 type = types.bool;
207 default = false;
208 description = lib.mdDoc "Turn off the message that sends when an alert returns to OK.";
209 };
210 settings = mkOption {
211 type = types.nullOr types.attrs;
212 default = null;
213 description = lib.mdDoc "Settings for the notifier type.";
214 };
215 secure_settings = mkOption {
216 type = types.nullOr types.attrs;
217 default = null;
218 description = lib.mdDoc ''
219 Secure settings for the notifier type. Please note that the contents of this option
220 will end up in a world-readable Nix store. Use the file provider
221 pointing at a reasonably secured file in the local filesystem
222 to work around that. Look at the documentation for details:
223 <https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider>
224 '';
225 };
226 };
227 };
228in
229{
230 imports = [
231 (mkRenamedOptionModule [ "services" "grafana" "protocol" ] [ "services" "grafana" "settings" "server" "protocol" ])
232 (mkRenamedOptionModule [ "services" "grafana" "addr" ] [ "services" "grafana" "settings" "server" "http_addr" ])
233 (mkRenamedOptionModule [ "services" "grafana" "port" ] [ "services" "grafana" "settings" "server" "http_port" ])
234 (mkRenamedOptionModule [ "services" "grafana" "domain" ] [ "services" "grafana" "settings" "server" "domain" ])
235 (mkRenamedOptionModule [ "services" "grafana" "rootUrl" ] [ "services" "grafana" "settings" "server" "root_url" ])
236 (mkRenamedOptionModule [ "services" "grafana" "staticRootPath" ] [ "services" "grafana" "settings" "server" "static_root_path" ])
237 (mkRenamedOptionModule [ "services" "grafana" "certFile" ] [ "services" "grafana" "settings" "server" "cert_file" ])
238 (mkRenamedOptionModule [ "services" "grafana" "certKey" ] [ "services" "grafana" "settings" "server" "cert_key" ])
239 (mkRenamedOptionModule [ "services" "grafana" "socket" ] [ "services" "grafana" "settings" "server" "socket" ])
240 (mkRenamedOptionModule [ "services" "grafana" "database" "type" ] [ "services" "grafana" "settings" "database" "type" ])
241 (mkRenamedOptionModule [ "services" "grafana" "database" "host" ] [ "services" "grafana" "settings" "database" "host" ])
242 (mkRenamedOptionModule [ "services" "grafana" "database" "name" ] [ "services" "grafana" "settings" "database" "name" ])
243 (mkRenamedOptionModule [ "services" "grafana" "database" "user" ] [ "services" "grafana" "settings" "database" "user" ])
244 (mkRenamedOptionModule [ "services" "grafana" "database" "password" ] [ "services" "grafana" "settings" "database" "password" ])
245 (mkRenamedOptionModule [ "services" "grafana" "database" "path" ] [ "services" "grafana" "settings" "database" "path" ])
246 (mkRenamedOptionModule [ "services" "grafana" "database" "connMaxLifetime" ] [ "services" "grafana" "settings" "database" "conn_max_lifetime" ])
247 (mkRenamedOptionModule [ "services" "grafana" "security" "adminUser" ] [ "services" "grafana" "settings" "security" "admin_user" ])
248 (mkRenamedOptionModule [ "services" "grafana" "security" "adminPassword" ] [ "services" "grafana" "settings" "security" "admin_password" ])
249 (mkRenamedOptionModule [ "services" "grafana" "security" "secretKey" ] [ "services" "grafana" "settings" "security" "secret_key" ])
250 (mkRenamedOptionModule [ "services" "grafana" "server" "serveFromSubPath" ] [ "services" "grafana" "settings" "server" "serve_from_sub_path" ])
251 (mkRenamedOptionModule [ "services" "grafana" "smtp" "enable" ] [ "services" "grafana" "settings" "smtp" "enabled" ])
252 (mkRenamedOptionModule [ "services" "grafana" "smtp" "user" ] [ "services" "grafana" "settings" "smtp" "user" ])
253 (mkRenamedOptionModule [ "services" "grafana" "smtp" "password" ] [ "services" "grafana" "settings" "smtp" "password" ])
254 (mkRenamedOptionModule [ "services" "grafana" "smtp" "fromAddress" ] [ "services" "grafana" "settings" "smtp" "from_address" ])
255 (mkRenamedOptionModule [ "services" "grafana" "users" "allowSignUp" ] [ "services" "grafana" "settings" "users" "allow_sign_up" ])
256 (mkRenamedOptionModule [ "services" "grafana" "users" "allowOrgCreate" ] [ "services" "grafana" "settings" "users" "allow_org_create" ])
257 (mkRenamedOptionModule [ "services" "grafana" "users" "autoAssignOrg" ] [ "services" "grafana" "settings" "users" "auto_assign_org" ])
258 (mkRenamedOptionModule [ "services" "grafana" "users" "autoAssignOrgRole" ] [ "services" "grafana" "settings" "users" "auto_assign_org_role" ])
259 (mkRenamedOptionModule [ "services" "grafana" "auth" "disableLoginForm" ] [ "services" "grafana" "settings" "auth" "disable_login_form" ])
260 (mkRenamedOptionModule [ "services" "grafana" "auth" "anonymous" "enable" ] [ "services" "grafana" "settings" "auth.anonymous" "enabled" ])
261 (mkRenamedOptionModule [ "services" "grafana" "auth" "anonymous" "org_name" ] [ "services" "grafana" "settings" "auth.anonymous" "org_name" ])
262 (mkRenamedOptionModule [ "services" "grafana" "auth" "anonymous" "org_role" ] [ "services" "grafana" "settings" "auth.anonymous" "org_role" ])
263 (mkRenamedOptionModule [ "services" "grafana" "auth" "azuread" "enable" ] [ "services" "grafana" "settings" "auth.azuread" "enabled" ])
264 (mkRenamedOptionModule [ "services" "grafana" "auth" "azuread" "allowSignUp" ] [ "services" "grafana" "settings" "auth.azuread" "allow_sign_up" ])
265 (mkRenamedOptionModule [ "services" "grafana" "auth" "azuread" "clientId" ] [ "services" "grafana" "settings" "auth.azuread" "client_id" ])
266 (mkRenamedOptionModule [ "services" "grafana" "auth" "azuread" "allowedDomains" ] [ "services" "grafana" "settings" "auth.azuread" "allowed_domains" ])
267 (mkRenamedOptionModule [ "services" "grafana" "auth" "azuread" "allowedGroups" ] [ "services" "grafana" "settings" "auth.azuread" "allowed_groups" ])
268 (mkRenamedOptionModule [ "services" "grafana" "auth" "google" "enable" ] [ "services" "grafana" "settings" "auth.google" "enabled" ])
269 (mkRenamedOptionModule [ "services" "grafana" "auth" "google" "allowSignUp" ] [ "services" "grafana" "settings" "auth.google" "allow_sign_up" ])
270 (mkRenamedOptionModule [ "services" "grafana" "auth" "google" "clientId" ] [ "services" "grafana" "settings" "auth.google" "client_id" ])
271 (mkRenamedOptionModule [ "services" "grafana" "analytics" "reporting" "enable" ] [ "services" "grafana" "settings" "analytics" "reporting_enabled" ])
272
273 (mkRemovedOptionModule [ "services" "grafana" "database" "passwordFile" ] ''
274 This option has been removed. Use 'services.grafana.settings.database.password' with file provider instead.
275 '')
276 (mkRemovedOptionModule [ "services" "grafana" "security" "adminPasswordFile" ] ''
277 This option has been removed. Use 'services.grafana.settings.security.admin_password' with file provider instead.
278 '')
279 (mkRemovedOptionModule [ "services" "grafana" "security" "secretKeyFile" ] ''
280 This option has been removed. Use 'services.grafana.settings.security.secret_key' with file provider instead.
281 '')
282 (mkRemovedOptionModule [ "services" "grafana" "smtp" "passwordFile" ] ''
283 This option has been removed. Use 'services.grafana.settings.smtp.password' with file provider instead.
284 '')
285 (mkRemovedOptionModule [ "services" "grafana" "auth" "azuread" "clientSecretFile" ] ''
286 This option has been removed. Use 'services.grafana.settings.azuread.client_secret' with file provider instead.
287 '')
288 (mkRemovedOptionModule [ "services" "grafana" "auth" "google" "clientSecretFile" ] ''
289 This option has been removed. Use 'services.grafana.settings.google.client_secret' with file provider instead.
290 '')
291 (mkRemovedOptionModule [ "services" "grafana" "extraOptions" ] ''
292 This option has been removed. Use 'services.grafana.settings' instead. For a detailed migration guide, please
293 review the release notes of NixOS 22.11.
294 '')
295
296 (mkRemovedOptionModule [ "services" "grafana" "auth" "azuread" "tenantId" ] "This option has been deprecated upstream.")
297 ];
298
299 options.services.grafana = {
300 enable = mkEnableOption (lib.mdDoc "grafana");
301
302 declarativePlugins = mkOption {
303 type = with types; nullOr (listOf path);
304 default = null;
305 description = lib.mdDoc "If non-null, then a list of packages containing Grafana plugins to install. If set, plugins cannot be manually installed.";
306 example = literalExpression "with pkgs.grafanaPlugins; [ grafana-piechart-panel ]";
307 # Make sure each plugin is added only once; otherwise building
308 # the link farm fails, since the same path is added multiple
309 # times.
310 apply = x: if isList x then lib.unique x else x;
311 };
312
313 package = mkOption {
314 description = lib.mdDoc "Package to use.";
315 default = pkgs.grafana;
316 defaultText = literalExpression "pkgs.grafana";
317 type = types.package;
318 };
319
320 dataDir = mkOption {
321 description = lib.mdDoc "Data directory.";
322 default = "/var/lib/grafana";
323 type = types.path;
324 };
325
326 settings = mkOption {
327 description = lib.mdDoc ''
328 Grafana settings. See <https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/>
329 for available options. INI format is used.
330 '';
331 type = types.submodule {
332 freeformType = settingsFormatIni.type;
333
334 options = {
335 paths = {
336 plugins = mkOption {
337 description = lib.mdDoc "Directory where grafana will automatically scan and look for plugins";
338 default = if (cfg.declarativePlugins == null) then "${cfg.dataDir}/plugins" else declarativePlugins;
339 defaultText = literalExpression "if (cfg.declarativePlugins == null) then \"\${cfg.dataDir}/plugins\" else declarativePlugins";
340 type = types.path;
341 };
342
343 provisioning = mkOption {
344 description = lib.mdDoc ''
345 Folder that contains provisioning config files that grafana will apply on startup and while running.
346 Don't change the value of this option if you are planning to use `services.grafana.provision` options.
347 '';
348 default = provisionConfDir;
349 defaultText = "directory with links to files generated from services.grafana.provision";
350 type = types.path;
351 };
352 };
353
354 server = {
355 protocol = mkOption {
356 description = lib.mdDoc "Which protocol to listen.";
357 default = "http";
358 type = types.enum [ "http" "https" "h2" "socket" ];
359 };
360
361 http_addr = mkOption {
362 type = types.str;
363 default = "127.0.0.1";
364 description = lib.mdDoc ''
365 Listening address.
366
367 ::: {.note}
368 This setting intentionally varies from upstream's default to be a bit more secure by default.
369 :::
370 '';
371 };
372
373 http_port = mkOption {
374 description = lib.mdDoc "Listening port.";
375 default = 3000;
376 type = types.port;
377 };
378
379 domain = mkOption {
380 description = lib.mdDoc ''
381 The public facing domain name used to access grafana from a browser.
382
383 This setting is only used in the default value of the `root_url` setting.
384 If you set the latter manually, this option does not have to be specified.
385 '';
386 default = "localhost";
387 type = types.str;
388 };
389
390 enforce_domain = mkOption {
391 description = lib.mdDoc ''
392 Redirect to correct domain if the host header does not match the domain.
393 Prevents DNS rebinding attacks.
394 '';
395 default = false;
396 type = types.bool;
397 };
398
399 root_url = mkOption {
400 description = lib.mdDoc ''
401 This is the full URL used to access Grafana from a web browser.
402 This is important if you use Google or GitHub OAuth authentication (for the callback URL to be correct).
403
404 This setting is also important if you have a reverse proxy in front of Grafana that exposes it through a subpath.
405 In that case add the subpath to the end of this URL setting.
406 '';
407 default = "%(protocol)s://%(domain)s:%(http_port)s/";
408 type = types.str;
409 };
410
411 serve_from_sub_path = mkOption {
412 description = lib.mdDoc ''
413 Serve Grafana from subpath specified in the `root_url` setting.
414 By default it is set to `false` for compatibility reasons.
415
416 By enabling this setting and using a subpath in `root_url` above,
417 e.g. `root_url = "http://localhost:3000/grafana"`,
418 Grafana is accessible on `http://localhost:3000/grafana`.
419 If accessed without subpath, Grafana will redirect to an URL with the subpath.
420 '';
421 default = false;
422 type = types.bool;
423 };
424
425 router_logging = mkOption {
426 description = lib.mdDoc ''
427 Set to `true` for Grafana to log all HTTP requests (not just errors).
428 These are logged as Info level events to the Grafana log.
429 '';
430 default = false;
431 type = types.bool;
432 };
433
434 static_root_path = mkOption {
435 description = lib.mdDoc "Root path for static assets.";
436 default = "${cfg.package}/share/grafana/public";
437 defaultText = literalExpression ''"''${package}/share/grafana/public"'';
438 type = types.str;
439 };
440
441 enable_gzip = mkOption {
442 description = lib.mdDoc ''
443 Set this option to `true` to enable HTTP compression, this can improve transfer speed and bandwidth utilization.
444 It is recommended that most users set it to `true`. By default it is set to `false` for compatibility reasons.
445 '';
446 default = false;
447 type = types.bool;
448 };
449
450 cert_file = mkOption {
451 description = lib.mdDoc ''
452 Path to the certificate file (if `protocol` is set to `https` or `h2`).
453 '';
454 default = null;
455 type = types.nullOr types.str;
456 };
457
458 cert_key = mkOption {
459 description = lib.mdDoc ''
460 Path to the certificate key file (if `protocol` is set to `https` or `h2`).
461 '';
462 default = null;
463 type = types.nullOr types.str;
464 };
465
466 socket_gid = mkOption {
467 description = lib.mdDoc ''
468 GID where the socket should be set when `protocol=socket`.
469 Make sure that the target group is in the group of Grafana process and that Grafana process is the file owner before you change this setting.
470 It is recommended to set the gid as http server user gid.
471 Not set when the value is -1.
472 '';
473 default = -1;
474 type = types.int;
475 };
476
477 socket_mode = mkOption {
478 description = lib.mdDoc ''
479 Mode where the socket should be set when `protocol=socket`.
480 Make sure that Grafana process is the file owner before you change this setting.
481 '';
482 # I assume this value is interpreted as octal literal by grafana.
483 # If this was an int, people following tutorials or porting their
484 # old config could stumble across nix not having octal literals.
485 default = "0660";
486 type = types.str;
487 };
488
489 socket = mkOption {
490 description = lib.mdDoc ''
491 Path where the socket should be created when `protocol=socket`.
492 Make sure that Grafana has appropriate permissions before you change this setting.
493 '';
494 default = "/run/grafana/grafana.sock";
495 type = types.str;
496 };
497
498 cdn_url = mkOption {
499 description = lib.mdDoc ''
500 Specify a full HTTP URL address to the root of your Grafana CDN assets.
501 Grafana will add edition and version paths.
502
503 For example, given a cdn url like `https://cdn.myserver.com`
504 grafana will try to load a javascript file from `http://cdn.myserver.com/grafana-oss/7.4.0/public/build/app.<hash>.js`.
505 '';
506 default = null;
507 type = types.nullOr types.str;
508 };
509
510 read_timeout = mkOption {
511 description = lib.mdDoc ''
512 Sets the maximum time using a duration format (5s/5m/5ms)
513 before timing out read of an incoming request and closing idle connections.
514 0 means there is no timeout for reading the request.
515 '';
516 default = "0";
517 type = types.str;
518 };
519 };
520
521 database = {
522 type = mkOption {
523 description = lib.mdDoc "Database type.";
524 default = "sqlite3";
525 type = types.enum [ "mysql" "sqlite3" "postgres" ];
526 };
527
528 host = mkOption {
529 description = lib.mdDoc ''
530 Only applicable to MySQL or Postgres.
531 Includes IP or hostname and port or in case of Unix sockets the path to it.
532 For example, for MySQL running on the same host as Grafana: `host = "127.0.0.1:3306"`
533 or with Unix sockets: `host = "/var/run/mysqld/mysqld.sock"`
534 '';
535 default = "127.0.0.1:3306";
536 type = types.str;
537 };
538
539 name = mkOption {
540 description = lib.mdDoc "The name of the Grafana database.";
541 default = "grafana";
542 type = types.str;
543 };
544
545 user = mkOption {
546 description = lib.mdDoc "The database user (not applicable for `sqlite3`).";
547 default = "root";
548 type = types.str;
549 };
550
551 password = mkOption {
552 description = lib.mdDoc ''
553 The database user's password (not applicable for `sqlite3`).
554
555 Please note that the contents of this option
556 will end up in a world-readable Nix store. Use the file provider
557 pointing at a reasonably secured file in the local filesystem
558 to work around that. Look at the documentation for details:
559 <https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider>
560 '';
561 default = "";
562 type = types.str;
563 };
564
565 max_idle_conn = mkOption {
566 description = lib.mdDoc "The maximum number of connections in the idle connection pool.";
567 default = 2;
568 type = types.int;
569 };
570
571 max_open_conn = mkOption {
572 description = lib.mdDoc "The maximum number of open connections to the database.";
573 default = 0;
574 type = types.int;
575 };
576
577 conn_max_lifetime = mkOption {
578 description = lib.mdDoc ''
579 Sets the maximum amount of time a connection may be reused.
580 The default is 14400 (which means 14400 seconds or 4 hours).
581 For MySQL, this setting should be shorter than the `wait_timeout` variable.
582 '';
583 default = 14400;
584 type = types.int;
585 };
586
587 locking_attempt_timeout_sec = mkOption {
588 description = lib.mdDoc ''
589 For `mysql`, if the `migrationLocking` feature toggle is set,
590 specify the time (in seconds) to wait before failing to lock the database for the migrations.
591 '';
592 default = 0;
593 type = types.int;
594 };
595
596 log_queries = mkOption {
597 description = lib.mdDoc "Set to `true` to log the sql calls and execution times";
598 default = false;
599 type = types.bool;
600 };
601
602 ssl_mode = mkOption {
603 description = lib.mdDoc ''
604 For Postgres, use either `disable`, `require` or `verify-full`.
605 For MySQL, use either `true`, `false`, or `skip-verify`.
606 '';
607 default = "disable";
608 type = types.enum [ "disable" "require" "verify-full" "true" "false" "skip-verify" ];
609 };
610
611 isolation_level = mkOption {
612 description = lib.mdDoc ''
613 Only the MySQL driver supports isolation levels in Grafana.
614 In case the value is empty, the driver's default isolation level is applied.
615 '';
616 default = null;
617 type = types.nullOr (types.enum [ "READ-UNCOMMITTED" "READ-COMMITTED" "REPEATABLE-READ" "SERIALIZABLE" ]);
618 };
619
620 ca_cert_path = mkOption {
621 description = lib.mdDoc "The path to the CA certificate to use.";
622 default = null;
623 type = types.nullOr types.str;
624 };
625
626 client_key_path = mkOption {
627 description = lib.mdDoc "The path to the client key. Only if server requires client authentication.";
628 default = null;
629 type = types.nullOr types.str;
630 };
631
632 client_cert_path = mkOption {
633 description = lib.mdDoc "The path to the client cert. Only if server requires client authentication.";
634 default = null;
635 type = types.nullOr types.str;
636 };
637
638 server_cert_name = mkOption {
639 description = lib.mdDoc ''
640 The common name field of the certificate used by the `mysql` or `postgres` server.
641 Not necessary if `ssl_mode` is set to `skip-verify`.
642 '';
643 default = null;
644 type = types.nullOr types.str;
645 };
646
647 path = mkOption {
648 description = lib.mdDoc "Only applicable to `sqlite3` database. The file path where the database will be stored.";
649 default = "${cfg.dataDir}/data/grafana.db";
650 defaultText = literalExpression ''"''${config.${opt.dataDir}}/data/grafana.db"'';
651 type = types.path;
652 };
653
654 cache_mode = mkOption {
655 description = lib.mdDoc ''
656 For `sqlite3` only.
657 [Shared cache](https://www.sqlite.org/sharedcache.html) setting used for connecting to the database.
658 '';
659 default = "private";
660 type = types.enum [ "private" "shared" ];
661 };
662
663 wal = mkOption {
664 description = lib.mdDoc ''
665 For `sqlite3` only.
666 Setting to enable/disable [Write-Ahead Logging](https://sqlite.org/wal.html).
667 '';
668 default = false;
669 type = types.bool;
670 };
671
672 query_retries = mkOption {
673 description = lib.mdDoc ''
674 This setting applies to `sqlite3` only and controls the number of times the system retries a query when the database is locked.
675 '';
676 default = 0;
677 type = types.int;
678 };
679
680 transaction_retries = mkOption {
681 description = lib.mdDoc ''
682 This setting applies to `sqlite3` only and controls the number of times the system retries a transaction when the database is locked.
683 '';
684 default = 5;
685 type = types.int;
686 };
687
688 # TODO Add "instrument_queries" option when upgrading to grafana 10.0
689 # instrument_queries = mkOption {
690 # description = lib.mdDoc "Set to `true` to add metrics and tracing for database queries.";
691 # default = false;
692 # type = types.bool;
693 # };
694 };
695
696 security = {
697 disable_initial_admin_creation = mkOption {
698 description = lib.mdDoc "Disable creation of admin user on first start of Grafana.";
699 default = false;
700 type = types.bool;
701 };
702
703 admin_user = mkOption {
704 description = lib.mdDoc "Default admin username.";
705 default = "admin";
706 type = types.str;
707 };
708
709 admin_password = mkOption {
710 description = lib.mdDoc ''
711 Default admin password. Please note that the contents of this option
712 will end up in a world-readable Nix store. Use the file provider
713 pointing at a reasonably secured file in the local filesystem
714 to work around that. Look at the documentation for details:
715 <https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider>
716 '';
717 default = "admin";
718 type = types.str;
719 };
720
721 admin_email = mkOption {
722 description = lib.mdDoc "The email of the default Grafana Admin, created on startup.";
723 default = "admin@localhost";
724 type = types.str;
725 };
726
727 secret_key = mkOption {
728 description = lib.mdDoc ''
729 Secret key used for signing. Please note that the contents of this option
730 will end up in a world-readable Nix store. Use the file provider
731 pointing at a reasonably secured file in the local filesystem
732 to work around that. Look at the documentation for details:
733 <https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider>
734 '';
735 default = "SW2YcwTIb9zpOOhoPsMm";
736 type = types.str;
737 };
738
739 disable_gravatar = mkOption {
740 description = lib.mdDoc "Set to `true` to disable the use of Gravatar for user profile images.";
741 default = false;
742 type = types.bool;
743 };
744
745 data_source_proxy_whitelist = mkOption {
746 description = lib.mdDoc ''
747 Define a whitelist of allowed IP addresses or domains, with ports,
748 to be used in data source URLs with the Grafana data source proxy.
749 Format: `ip_or_domain:port` separated by spaces.
750 PostgreSQL, MySQL, and MSSQL data sources do not use the proxy and are therefore unaffected by this setting.
751 '';
752 default = [ ];
753 type = types.oneOf [ types.str (types.listOf types.str) ];
754 };
755
756 disable_brute_force_login_protection = mkOption {
757 description = lib.mdDoc "Set to `true` to disable [brute force login protection](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#account-lockout).";
758 default = false;
759 type = types.bool;
760 };
761
762 cookie_secure = mkOption {
763 description = lib.mdDoc "Set to `true` if you host Grafana behind HTTPS.";
764 default = false;
765 type = types.bool;
766 };
767
768 cookie_samesite = mkOption {
769 description = lib.mdDoc ''
770 Sets the `SameSite` cookie attribute and prevents the browser from sending this cookie along with cross-site requests.
771 The main goal is to mitigate the risk of cross-origin information leakage.
772 This setting also provides some protection against cross-site request forgery attacks (CSRF),
773 [read more about SameSite here](https://owasp.org/www-community/SameSite).
774 Using value `disabled` does not add any `SameSite` attribute to cookies.
775 '';
776 default = "lax";
777 type = types.enum [ "lax" "strict" "none" "disabled" ];
778 };
779
780 allow_embedding = mkOption {
781 description = lib.mdDoc ''
782 When `false`, the HTTP header `X-Frame-Options: deny` will be set in Grafana HTTP responses
783 which will instruct browsers to not allow rendering Grafana in a `<frame>`, `<iframe>`, `<embed>` or `<object>`.
784 The main goal is to mitigate the risk of [Clickjacking](https://owasp.org/www-community/attacks/Clickjacking).
785 '';
786 default = false;
787 type = types.bool;
788 };
789
790 strict_transport_security = mkOption {
791 description = lib.mdDoc ''
792 Set to `true` if you want to enable HTTP `Strict-Transport-Security` (HSTS) response header.
793 Only use this when HTTPS is enabled in your configuration,
794 or when there is another upstream system that ensures your application does HTTPS (like a frontend load balancer).
795 HSTS tells browsers that the site should only be accessed using HTTPS.
796 '';
797 default = false;
798 type = types.bool;
799 };
800
801 strict_transport_security_max_age_seconds = mkOption {
802 description = lib.mdDoc ''
803 Sets how long a browser should cache HSTS in seconds.
804 Only applied if `strict_transport_security` is enabled.
805 '';
806 default = 86400;
807 type = types.int;
808 };
809
810 strict_transport_security_preload = mkOption {
811 description = lib.mdDoc ''
812 Set to `true` to enable HSTS `preloading` option.
813 Only applied if `strict_transport_security` is enabled.
814 '';
815 default = false;
816 type = types.bool;
817 };
818
819 strict_transport_security_subdomains = mkOption {
820 description = lib.mdDoc ''
821 Set to `true` to enable HSTS `includeSubDomains` option.
822 Only applied if `strict_transport_security` is enabled.
823 '';
824 default = false;
825 type = types.bool;
826 };
827
828 x_content_type_options = mkOption {
829 description = lib.mdDoc ''
830 Set to `false` to disable the `X-Content-Type-Options` response header.
831 The `X-Content-Type-Options` response HTTP header is a marker used by the server
832 to indicate that the MIME types advertised in the `Content-Type` headers should not be changed and be followed.
833 '';
834 default = true;
835 type = types.bool;
836 };
837
838 x_xss_protection = mkOption {
839 description = lib.mdDoc ''
840 Set to `false` to disable the `X-XSS-Protection` header,
841 which tells browsers to stop pages from loading when they detect reflected cross-site scripting (XSS) attacks.
842 '';
843 default = true;
844 type = types.bool;
845 };
846
847 content_security_policy = mkOption {
848 description = lib.mdDoc ''
849 Set to `true` to add the `Content-Security-Policy` header to your requests.
850 CSP allows to control resources that the user agent can load and helps prevent XSS attacks.
851 '';
852 default = false;
853 type = types.bool;
854 };
855
856 content_security_policy_report_only = mkOption {
857 description = lib.mdDoc ''
858 Set to `true` to add the `Content-Security-Policy-Report-Only` header to your requests.
859 CSP in Report Only mode enables you to experiment with policies by monitoring their effects without enforcing them.
860 You can enable both policies simultaneously.
861 '';
862 default = false;
863 type = types.bool;
864 };
865
866 # The options content_security_policy_template and
867 # content_security_policy_template are missing because I'm not sure
868 # how exactly the quoting of the default value works. See also
869 # https://github.com/grafana/grafana/blob/cb7e18938b8eb6860a64b91aaba13a7eb31bc95b/conf/defaults.ini#L364
870 # https://github.com/grafana/grafana/blob/cb7e18938b8eb6860a64b91aaba13a7eb31bc95b/conf/defaults.ini#L373
871
872 # These two options are lists joined with spaces:
873 # https://github.com/grafana/grafana/blob/916d9793aa81c2990640b55a15dee0db6b525e41/pkg/middleware/csrf/csrf.go#L37-L38
874
875 csrf_trusted_origins = mkOption {
876 description = lib.mdDoc ''
877 List of additional allowed URLs to pass by the CSRF check.
878 Suggested when authentication comes from an IdP.
879 '';
880 default = [ ];
881 type = types.oneOf [ types.str (types.listOf types.str) ];
882 };
883
884 csrf_additional_headers = mkOption {
885 description = lib.mdDoc ''
886 List of allowed headers to be set by the user.
887 Suggested to use for if authentication lives behind reverse proxies.
888 '';
889 default = [ ];
890 type = types.oneOf [ types.str (types.listOf types.str) ];
891 };
892 };
893
894 smtp = {
895 enabled = mkOption {
896 description = lib.mdDoc "Whether to enable SMTP.";
897 default = false;
898 type = types.bool;
899 };
900
901 host = mkOption {
902 description = lib.mdDoc "Host to connect to.";
903 default = "localhost:25";
904 type = types.str;
905 };
906
907 user = mkOption {
908 description = lib.mdDoc "User used for authentication.";
909 default = null;
910 type = types.nullOr types.str;
911 };
912
913 password = mkOption {
914 description = lib.mdDoc ''
915 Password used for authentication. Please note that the contents of this option
916 will end up in a world-readable Nix store. Use the file provider
917 pointing at a reasonably secured file in the local filesystem
918 to work around that. Look at the documentation for details:
919 <https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider>
920 '';
921 default = "";
922 type = types.str;
923 };
924
925 cert_file = mkOption {
926 description = lib.mdDoc "File path to a cert file.";
927 default = null;
928 type = types.nullOr types.str;
929 };
930
931 key_file = mkOption {
932 description = lib.mdDoc "File path to a key file.";
933 default = null;
934 type = types.nullOr types.str;
935 };
936
937 skip_verify = mkOption {
938 description = lib.mdDoc "Verify SSL for SMTP server.";
939 default = false;
940 type = types.bool;
941 };
942
943 from_address = mkOption {
944 description = lib.mdDoc "Address used when sending out emails.";
945 default = "admin@grafana.localhost";
946 type = types.str;
947 };
948
949 from_name = mkOption {
950 description = lib.mdDoc "Name to be used as client identity for EHLO in SMTP dialog.";
951 default = "Grafana";
952 type = types.str;
953 };
954
955 ehlo_identity = mkOption {
956 description = lib.mdDoc "Name to be used as client identity for EHLO in SMTP dialog.";
957 default = null;
958 type = types.nullOr types.str;
959 };
960
961 startTLS_policy = mkOption {
962 description = lib.mdDoc "StartTLS policy when connecting to server.";
963 default = null;
964 type = types.nullOr (types.enum [ "OpportunisticStartTLS" "MandatoryStartTLS" "NoStartTLS" ]);
965 };
966 };
967
968 users = {
969 allow_sign_up = mkOption {
970 description = lib.mdDoc ''
971 Set to false to prohibit users from being able to sign up / create user accounts.
972 The admin user can still create users.
973 '';
974 default = false;
975 type = types.bool;
976 };
977
978 allow_org_create = mkOption {
979 description = lib.mdDoc "Set to `false` to prohibit users from creating new organizations.";
980 default = false;
981 type = types.bool;
982 };
983
984 auto_assign_org = mkOption {
985 description = lib.mdDoc ''
986 Set to `true` to automatically add new users to the main organization (id 1).
987 When set to `false,` new users automatically cause a new organization to be created for that new user.
988 The organization will be created even if the `allow_org_create` setting is set to `false`.
989 '';
990 default = true;
991 type = types.bool;
992 };
993
994 auto_assign_org_id = mkOption {
995 description = lib.mdDoc ''
996 Set this value to automatically add new users to the provided org.
997 This requires `auto_assign_org` to be set to `true`.
998 Please make sure that this organization already exists.
999 '';
1000 default = 1;
1001 type = types.int;
1002 };
1003
1004 auto_assign_org_role = mkOption {
1005 description = lib.mdDoc ''
1006 The role new users will be assigned for the main organization (if the `auto_assign_org` setting is set to `true`).
1007 '';
1008 default = "Viewer";
1009 type = types.enum [ "Viewer" "Editor" "Admin" ];
1010 };
1011
1012 verify_email_enabled = mkOption {
1013 description = lib.mdDoc "Require email validation before sign up completes.";
1014 default = false;
1015 type = types.bool;
1016 };
1017
1018 login_hint = mkOption {
1019 description = lib.mdDoc "Text used as placeholder text on login page for login/username input.";
1020 default = "email or username";
1021 type = types.str;
1022 };
1023
1024 password_hint = mkOption {
1025 description = lib.mdDoc "Text used as placeholder text on login page for password input.";
1026 default = "password";
1027 type = types.str;
1028 };
1029
1030 default_theme = mkOption {
1031 description = lib.mdDoc "Sets the default UI theme. `system` matches the user's system theme.";
1032 default = "dark";
1033 type = types.enum [ "dark" "light" "system" ];
1034 };
1035
1036 default_language = mkOption {
1037 description = lib.mdDoc "This setting configures the default UI language, which must be a supported IETF language tag, such as `en-US`.";
1038 default = "en-US";
1039 type = types.str;
1040 };
1041
1042 home_page = mkOption {
1043 description = lib.mdDoc ''
1044 Path to a custom home page.
1045 Users are only redirected to this if the default home dashboard is used.
1046 It should match a frontend route and contain a leading slash.
1047 '';
1048 default = "";
1049 type = types.str;
1050 };
1051
1052 viewers_can_edit = mkOption {
1053 description = lib.mdDoc ''
1054 Viewers can access and use Explore and perform temporary edits on panels in dashboards they have access to.
1055 They cannot save their changes.
1056 '';
1057 default = false;
1058 type = types.bool;
1059 };
1060
1061 editors_can_admin = mkOption {
1062 description = lib.mdDoc "Editors can administrate dashboards, folders and teams they create.";
1063 default = false;
1064 type = types.bool;
1065 };
1066
1067 user_invite_max_lifetime_duration = mkOption {
1068 description = lib.mdDoc ''
1069 The duration in time a user invitation remains valid before expiring.
1070 This setting should be expressed as a duration.
1071 Examples: `6h` (hours), `2d` (days), `1w` (week).
1072 The minimum supported duration is `15m` (15 minutes).
1073 '';
1074 default = "24h";
1075 type = types.str;
1076 };
1077
1078 # Lists are joined via space, so this option can't be a list.
1079 # Users have to manually join their values.
1080 hidden_users = mkOption {
1081 description = lib.mdDoc ''
1082 This is a comma-separated list of usernames.
1083 Users specified here are hidden in the Grafana UI.
1084 They are still visible to Grafana administrators and to themselves.
1085 '';
1086 default = "";
1087 type = types.str;
1088 };
1089 };
1090
1091 analytics = {
1092 reporting_enabled = mkOption {
1093 description = lib.mdDoc ''
1094 When enabled Grafana will send anonymous usage statistics to `stats.grafana.org`.
1095 No IP addresses are being tracked, only simple counters to track running instances, versions, dashboard and error counts.
1096 Counters are sent every 24 hours.
1097 '';
1098 default = true;
1099 type = types.bool;
1100 };
1101
1102 check_for_updates = mkOption {
1103 description = lib.mdDoc ''
1104 When set to `false`, disables checking for new versions of Grafana from Grafana's GitHub repository.
1105 When enabled, the check for a new version runs every 10 minutes.
1106 It will notify, via the UI, when a new version is available.
1107 The check itself will not prompt any auto-updates of the Grafana software, nor will it send any sensitive information.
1108 '';
1109 default = false;
1110 type = types.bool;
1111 };
1112
1113 check_for_plugin_updates = mkOption {
1114 description = lib.mdDoc ''
1115 When set to `false`, disables checking for new versions of installed plugins from https://grafana.com.
1116 When enabled, the check for a new plugin runs every 10 minutes.
1117 It will notify, via the UI, when a new plugin update exists.
1118 The check itself will not prompt any auto-updates of the plugin, nor will it send any sensitive information.
1119 '';
1120 default = cfg.declarativePlugins == null;
1121 defaultText = literalExpression "cfg.declarativePlugins == null";
1122 type = types.bool;
1123 };
1124
1125 feedback_links_enabled = mkOption {
1126 description = lib.mdDoc "Set to `false` to remove all feedback links from the UI.";
1127 default = true;
1128 type = types.bool;
1129 };
1130 };
1131 };
1132 };
1133 };
1134
1135 provision = {
1136 enable = mkEnableOption (lib.mdDoc "provision");
1137
1138 datasources = mkOption {
1139 description = lib.mdDoc ''
1140 Declaratively provision Grafana's datasources.
1141 '';
1142 default = { };
1143 type = types.submodule {
1144 options.settings = mkOption {
1145 description = lib.mdDoc ''
1146 Grafana datasource configuration in Nix. Can't be used with
1147 [](#opt-services.grafana.provision.datasources.path) simultaneously. See
1148 <https://grafana.com/docs/grafana/latest/administration/provisioning/#data-sources>
1149 for supported options.
1150 '';
1151 default = null;
1152 type = types.nullOr (types.submodule {
1153 options = {
1154 apiVersion = mkOption {
1155 description = lib.mdDoc "Config file version.";
1156 default = 1;
1157 type = types.int;
1158 };
1159
1160 datasources = mkOption {
1161 description = lib.mdDoc "List of datasources to insert/update.";
1162 default = [ ];
1163 type = types.listOf grafanaTypes.datasourceConfig;
1164 };
1165
1166 deleteDatasources = mkOption {
1167 description = lib.mdDoc "List of datasources that should be deleted from the database.";
1168 default = [ ];
1169 type = types.listOf (types.submodule {
1170 options.name = mkOption {
1171 description = lib.mdDoc "Name of the datasource to delete.";
1172 type = types.str;
1173 };
1174
1175 options.orgId = mkOption {
1176 description = lib.mdDoc "Organization ID of the datasource to delete.";
1177 type = types.int;
1178 };
1179 });
1180 };
1181 };
1182 });
1183 example = literalExpression ''
1184 {
1185 apiVersion = 1;
1186
1187 datasources = [{
1188 name = "Graphite";
1189 type = "graphite";
1190 }];
1191
1192 deleteDatasources = [{
1193 name = "Graphite";
1194 orgId = 1;
1195 }];
1196 }
1197 '';
1198 };
1199
1200 options.path = mkOption {
1201 description = lib.mdDoc ''
1202 Path to YAML datasource configuration. Can't be used with
1203 [](#opt-services.grafana.provision.datasources.settings) simultaneously.
1204 Can be either a directory or a single YAML file. Will end up in the store.
1205 '';
1206 default = null;
1207 type = types.nullOr types.path;
1208 };
1209 };
1210 };
1211
1212
1213 dashboards = mkOption {
1214 description = lib.mdDoc ''
1215 Declaratively provision Grafana's dashboards.
1216 '';
1217 default = { };
1218 type = types.submodule {
1219 options.settings = mkOption {
1220 description = lib.mdDoc ''
1221 Grafana dashboard configuration in Nix. Can't be used with
1222 [](#opt-services.grafana.provision.dashboards.path) simultaneously. See
1223 <https://grafana.com/docs/grafana/latest/administration/provisioning/#dashboards>
1224 for supported options.
1225 '';
1226 default = null;
1227 type = types.nullOr (types.submodule {
1228 options.apiVersion = mkOption {
1229 description = lib.mdDoc "Config file version.";
1230 default = 1;
1231 type = types.int;
1232 };
1233
1234 options.providers = mkOption {
1235 description = lib.mdDoc "List of dashboards to insert/update.";
1236 default = [ ];
1237 type = types.listOf grafanaTypes.dashboardConfig;
1238 };
1239 });
1240 example = literalExpression ''
1241 {
1242 apiVersion = 1;
1243
1244 providers = [{
1245 name = "default";
1246 options.path = "/var/lib/grafana/dashboards";
1247 }];
1248 }
1249 '';
1250 };
1251
1252 options.path = mkOption {
1253 description = lib.mdDoc ''
1254 Path to YAML dashboard configuration. Can't be used with
1255 [](#opt-services.grafana.provision.dashboards.settings) simultaneously.
1256 Can be either a directory or a single YAML file. Will end up in the store.
1257 '';
1258 default = null;
1259 type = types.nullOr types.path;
1260 };
1261 };
1262 };
1263
1264
1265 notifiers = mkOption {
1266 description = lib.mdDoc "Grafana notifier configuration.";
1267 default = [ ];
1268 type = types.listOf grafanaTypes.notifierConfig;
1269 apply = x: map _filter x;
1270 };
1271
1272
1273 alerting = {
1274 rules = {
1275 path = mkOption {
1276 description = lib.mdDoc ''
1277 Path to YAML rules configuration. Can't be used with
1278 [](#opt-services.grafana.provision.alerting.rules.settings) simultaneously.
1279 Can be either a directory or a single YAML file. Will end up in the store.
1280 '';
1281 default = null;
1282 type = types.nullOr types.path;
1283 };
1284
1285 settings = mkOption {
1286 description = lib.mdDoc ''
1287 Grafana rules configuration in Nix. Can't be used with
1288 [](#opt-services.grafana.provision.alerting.rules.path) simultaneously. See
1289 <https://grafana.com/docs/grafana/latest/administration/provisioning/#rules>
1290 for supported options.
1291 '';
1292 default = null;
1293 type = types.nullOr (types.submodule {
1294 options = {
1295 apiVersion = mkOption {
1296 description = lib.mdDoc "Config file version.";
1297 default = 1;
1298 type = types.int;
1299 };
1300
1301 groups = mkOption {
1302 description = lib.mdDoc "List of rule groups to import or update.";
1303 default = [ ];
1304 type = types.listOf (types.submodule {
1305 freeformType = provisioningSettingsFormat.type;
1306
1307 options.name = mkOption {
1308 description = lib.mdDoc "Name of the rule group. Required.";
1309 type = types.str;
1310 };
1311
1312 options.folder = mkOption {
1313 description = lib.mdDoc "Name of the folder the rule group will be stored in. Required.";
1314 type = types.str;
1315 };
1316
1317 options.interval = mkOption {
1318 description = lib.mdDoc "Interval that the rule group should be evaluated at. Required.";
1319 type = types.str;
1320 };
1321 });
1322 };
1323
1324 deleteRules = mkOption {
1325 description = lib.mdDoc "List of alert rule UIDs that should be deleted.";
1326 default = [ ];
1327 type = types.listOf (types.submodule {
1328 options.orgId = mkOption {
1329 description = lib.mdDoc "Organization ID, default = 1";
1330 default = 1;
1331 type = types.int;
1332 };
1333
1334 options.uid = mkOption {
1335 description = lib.mdDoc "Unique identifier for the rule. Required.";
1336 type = types.str;
1337 };
1338 });
1339 };
1340 };
1341 });
1342 example = literalExpression ''
1343 {
1344 apiVersion = 1;
1345
1346 groups = [{
1347 orgId = 1;
1348 name = "my_rule_group";
1349 folder = "my_first_folder";
1350 interval = "60s";
1351 rules = [{
1352 uid = "my_id_1";
1353 title = "my_first_rule";
1354 condition = "A";
1355 data = [{
1356 refId = "A";
1357 datasourceUid = "-100";
1358 model = {
1359 conditions = [{
1360 evaluator = {
1361 params = [ 3 ];
1362 type = "git";
1363 };
1364 operator.type = "and";
1365 query.params = [ "A" ];
1366 reducer.type = "last";
1367 type = "query";
1368 }];
1369 datasource = {
1370 type = "__expr__";
1371 uid = "-100";
1372 };
1373 expression = "1==0";
1374 intervalMs = 1000;
1375 maxDataPoints = 43200;
1376 refId = "A";
1377 type = "math";
1378 };
1379 }];
1380 dashboardUid = "my_dashboard";
1381 panelId = 123;
1382 noDataState = "Alerting";
1383 for = "60s";
1384 annotations.some_key = "some_value";
1385 labels.team = "sre_team1";
1386 }];
1387 }];
1388
1389 deleteRules = [{
1390 orgId = 1;
1391 uid = "my_id_1";
1392 }];
1393 }
1394 '';
1395 };
1396 };
1397
1398 contactPoints = {
1399 path = mkOption {
1400 description = lib.mdDoc ''
1401 Path to YAML contact points configuration. Can't be used with
1402 [](#opt-services.grafana.provision.alerting.contactPoints.settings) simultaneously.
1403 Can be either a directory or a single YAML file. Will end up in the store.
1404 '';
1405 default = null;
1406 type = types.nullOr types.path;
1407 };
1408
1409 settings = mkOption {
1410 description = lib.mdDoc ''
1411 Grafana contact points configuration in Nix. Can't be used with
1412 [](#opt-services.grafana.provision.alerting.contactPoints.path) simultaneously. See
1413 <https://grafana.com/docs/grafana/latest/administration/provisioning/#contact-points>
1414 for supported options.
1415 '';
1416 default = null;
1417 type = types.nullOr (types.submodule {
1418 options = {
1419 apiVersion = mkOption {
1420 description = lib.mdDoc "Config file version.";
1421 default = 1;
1422 type = types.int;
1423 };
1424
1425 contactPoints = mkOption {
1426 description = lib.mdDoc "List of contact points to import or update.";
1427 default = [ ];
1428 type = types.listOf (types.submodule {
1429 freeformType = provisioningSettingsFormat.type;
1430
1431 options.name = mkOption {
1432 description = lib.mdDoc "Name of the contact point. Required.";
1433 type = types.str;
1434 };
1435 });
1436 };
1437
1438 deleteContactPoints = mkOption {
1439 description = lib.mdDoc "List of receivers that should be deleted.";
1440 default = [ ];
1441 type = types.listOf (types.submodule {
1442 options.orgId = mkOption {
1443 description = lib.mdDoc "Organization ID, default = 1.";
1444 default = 1;
1445 type = types.int;
1446 };
1447
1448 options.uid = mkOption {
1449 description = lib.mdDoc "Unique identifier for the receiver. Required.";
1450 type = types.str;
1451 };
1452 });
1453 };
1454 };
1455 });
1456 example = literalExpression ''
1457 {
1458 apiVersion = 1;
1459
1460 contactPoints = [{
1461 orgId = 1;
1462 name = "cp_1";
1463 receivers = [{
1464 uid = "first_uid";
1465 type = "prometheus-alertmanager";
1466 settings.url = "http://test:9000";
1467 }];
1468 }];
1469
1470 deleteContactPoints = [{
1471 orgId = 1;
1472 uid = "first_uid";
1473 }];
1474 }
1475 '';
1476 };
1477 };
1478
1479 policies = {
1480 path = mkOption {
1481 description = lib.mdDoc ''
1482 Path to YAML notification policies configuration. Can't be used with
1483 [](#opt-services.grafana.provision.alerting.policies.settings) simultaneously.
1484 Can be either a directory or a single YAML file. Will end up in the store.
1485 '';
1486 default = null;
1487 type = types.nullOr types.path;
1488 };
1489
1490 settings = mkOption {
1491 description = lib.mdDoc ''
1492 Grafana notification policies configuration in Nix. Can't be used with
1493 [](#opt-services.grafana.provision.alerting.policies.path) simultaneously. See
1494 <https://grafana.com/docs/grafana/latest/administration/provisioning/#notification-policies>
1495 for supported options.
1496 '';
1497 default = null;
1498 type = types.nullOr (types.submodule {
1499 options = {
1500 apiVersion = mkOption {
1501 description = lib.mdDoc "Config file version.";
1502 default = 1;
1503 type = types.int;
1504 };
1505
1506 policies = mkOption {
1507 description = lib.mdDoc "List of contact points to import or update.";
1508 default = [ ];
1509 type = types.listOf (types.submodule {
1510 freeformType = provisioningSettingsFormat.type;
1511 });
1512 };
1513
1514 resetPolicies = mkOption {
1515 description = lib.mdDoc "List of orgIds that should be reset to the default policy.";
1516 default = [ ];
1517 type = types.listOf types.int;
1518 };
1519 };
1520 });
1521 example = literalExpression ''
1522 {
1523 apiVersion = 1;
1524
1525 policies = [{
1526 orgId = 1;
1527 receiver = "grafana-default-email";
1528 group_by = [ "..." ];
1529 matchers = [
1530 "alertname = Watchdog"
1531 "severity =~ \"warning|critical\""
1532 ];
1533 mute_time_intervals = [
1534 "abc"
1535 ];
1536 group_wait = "30s";
1537 group_interval = "5m";
1538 repeat_interval = "4h";
1539 }];
1540
1541 resetPolicies = [
1542 1
1543 ];
1544 }
1545 '';
1546 };
1547 };
1548
1549 templates = {
1550 path = mkOption {
1551 description = lib.mdDoc ''
1552 Path to YAML templates configuration. Can't be used with
1553 [](#opt-services.grafana.provision.alerting.templates.settings) simultaneously.
1554 Can be either a directory or a single YAML file. Will end up in the store.
1555 '';
1556 default = null;
1557 type = types.nullOr types.path;
1558 };
1559
1560 settings = mkOption {
1561 description = lib.mdDoc ''
1562 Grafana templates configuration in Nix. Can't be used with
1563 [](#opt-services.grafana.provision.alerting.templates.path) simultaneously. See
1564 <https://grafana.com/docs/grafana/latest/administration/provisioning/#templates>
1565 for supported options.
1566 '';
1567 default = null;
1568 type = types.nullOr (types.submodule {
1569 options = {
1570 apiVersion = mkOption {
1571 description = lib.mdDoc "Config file version.";
1572 default = 1;
1573 type = types.int;
1574 };
1575
1576 templates = mkOption {
1577 description = lib.mdDoc "List of templates to import or update.";
1578 default = [ ];
1579 type = types.listOf (types.submodule {
1580 freeformType = provisioningSettingsFormat.type;
1581
1582 options.name = mkOption {
1583 description = lib.mdDoc "Name of the template, must be unique. Required.";
1584 type = types.str;
1585 };
1586
1587 options.template = mkOption {
1588 description = lib.mdDoc "Alerting with a custom text template";
1589 type = types.str;
1590 };
1591 });
1592 };
1593
1594 deleteTemplates = mkOption {
1595 description = lib.mdDoc "List of alert rule UIDs that should be deleted.";
1596 default = [ ];
1597 type = types.listOf (types.submodule {
1598 options.orgId = mkOption {
1599 description = lib.mdDoc "Organization ID, default = 1.";
1600 default = 1;
1601 type = types.int;
1602 };
1603
1604 options.name = mkOption {
1605 description = lib.mdDoc "Name of the template, must be unique. Required.";
1606 type = types.str;
1607 };
1608 });
1609 };
1610 };
1611 });
1612 example = literalExpression ''
1613 {
1614 apiVersion = 1;
1615
1616 templates = [{
1617 orgId = 1;
1618 name = "my_first_template";
1619 template = "Alerting with a custom text template";
1620 }];
1621
1622 deleteTemplates = [{
1623 orgId = 1;
1624 name = "my_first_template";
1625 }];
1626 }
1627 '';
1628 };
1629 };
1630
1631 muteTimings = {
1632 path = mkOption {
1633 description = lib.mdDoc ''
1634 Path to YAML mute timings configuration. Can't be used with
1635 [](#opt-services.grafana.provision.alerting.muteTimings.settings) simultaneously.
1636 Can be either a directory or a single YAML file. Will end up in the store.
1637 '';
1638 default = null;
1639 type = types.nullOr types.path;
1640 };
1641
1642 settings = mkOption {
1643 description = lib.mdDoc ''
1644 Grafana mute timings configuration in Nix. Can't be used with
1645 [](#opt-services.grafana.provision.alerting.muteTimings.path) simultaneously. See
1646 <https://grafana.com/docs/grafana/latest/administration/provisioning/#mute-timings>
1647 for supported options.
1648 '';
1649 default = null;
1650 type = types.nullOr (types.submodule {
1651 options = {
1652 apiVersion = mkOption {
1653 description = lib.mdDoc "Config file version.";
1654 default = 1;
1655 type = types.int;
1656 };
1657
1658 muteTimes = mkOption {
1659 description = lib.mdDoc "List of mute time intervals to import or update.";
1660 default = [ ];
1661 type = types.listOf (types.submodule {
1662 freeformType = provisioningSettingsFormat.type;
1663
1664 options.name = mkOption {
1665 description = lib.mdDoc "Name of the mute time interval, must be unique. Required.";
1666 type = types.str;
1667 };
1668 });
1669 };
1670
1671 deleteMuteTimes = mkOption {
1672 description = lib.mdDoc "List of mute time intervals that should be deleted.";
1673 default = [ ];
1674 type = types.listOf (types.submodule {
1675 options.orgId = mkOption {
1676 description = lib.mdDoc "Organization ID, default = 1.";
1677 default = 1;
1678 type = types.int;
1679 };
1680
1681 options.name = mkOption {
1682 description = lib.mdDoc "Name of the mute time interval, must be unique. Required.";
1683 type = types.str;
1684 };
1685 });
1686 };
1687 };
1688 });
1689 example = literalExpression ''
1690 {
1691 apiVersion = 1;
1692
1693 muteTimes = [{
1694 orgId = 1;
1695 name = "mti_1";
1696 time_intervals = [{
1697 times = [{
1698 start_time = "06:00";
1699 end_time = "23:59";
1700 }];
1701 weekdays = [
1702 "monday:wednesday"
1703 "saturday"
1704 "sunday"
1705 ];
1706 months = [
1707 "1:3"
1708 "may:august"
1709 "december"
1710 ];
1711 years = [
1712 "2020:2022"
1713 "2030"
1714 ];
1715 days_of_month = [
1716 "1:5"
1717 "-3:-1"
1718 ];
1719 }];
1720 }];
1721
1722 deleteMuteTimes = [{
1723 orgId = 1;
1724 name = "mti_1";
1725 }];
1726 }
1727 '';
1728 };
1729 };
1730 };
1731 };
1732 };
1733
1734 config = mkIf cfg.enable {
1735 warnings =
1736 let
1737 doesntUseFileProvider = opt: defaultValue:
1738 let regex = "${optionalString (defaultValue != null) "^${defaultValue}$|"}^\\$__(file|env)\\{.*}$|^\\$[^_\\$][^ ]+$";
1739 in builtins.match regex opt == null;
1740
1741 # Ensure that no custom credentials are leaked into the Nix store. Unless the default value
1742 # is specified, this can be achieved by using the file/env provider:
1743 # https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#variable-expansion
1744 passwordWithoutFileProvider = optional
1745 (
1746 doesntUseFileProvider cfg.settings.database.password "" ||
1747 doesntUseFileProvider cfg.settings.security.admin_password "admin"
1748 )
1749 ''
1750 Grafana passwords will be stored as plaintext in the Nix store!
1751 Use file provider or an env-var instead.
1752 '';
1753
1754 # Warn about deprecated notifiers.
1755 deprecatedNotifiers = optional (cfg.provision.notifiers != [ ]) ''
1756 Notifiers are deprecated upstream and will be removed in Grafana 11.
1757 Use `services.grafana.provision.alerting.contactPoints` instead.
1758 '';
1759
1760 # Ensure that `secureJsonData` of datasources provisioned via `datasources.settings`
1761 # only uses file/env providers.
1762 secureJsonDataWithoutFileProvider = optional
1763 (
1764 let
1765 datasourcesToCheck = optionals
1766 (cfg.provision.datasources.settings != null)
1767 cfg.provision.datasources.settings.datasources;
1768 declarationUnsafe = { secureJsonData, ... }:
1769 secureJsonData != null
1770 && any (flip doesntUseFileProvider null) (attrValues secureJsonData);
1771 in
1772 any declarationUnsafe datasourcesToCheck
1773 )
1774 ''
1775 Declarations in the `secureJsonData`-block of a datasource will be leaked to the
1776 Nix store unless a file-provider or an env-var is used!
1777 '';
1778
1779 notifierSecureSettingsWithoutFileProvider = optional
1780 (any (x: x.secure_settings != null) cfg.provision.notifiers)
1781 "Notifier secure settings will be stored as plaintext in the Nix store! Use file provider instead.";
1782 in
1783 passwordWithoutFileProvider
1784 ++ deprecatedNotifiers
1785 ++ secureJsonDataWithoutFileProvider
1786 ++ notifierSecureSettingsWithoutFileProvider;
1787
1788 environment.systemPackages = [ cfg.package ];
1789
1790 assertions = [
1791 {
1792 assertion = cfg.provision.datasources.settings == null || cfg.provision.datasources.path == null;
1793 message = "Cannot set both datasources settings and datasources path";
1794 }
1795 {
1796 assertion =
1797 let
1798 prometheusIsNotDirect = opt: all
1799 ({ type, access, ... }: type == "prometheus" -> access != "direct")
1800 opt;
1801 in
1802 cfg.provision.datasources.settings == null || prometheusIsNotDirect cfg.provision.datasources.settings.datasources;
1803 message = "For datasources of type `prometheus`, the `direct` access mode is not supported anymore (since Grafana 9.2.0)";
1804 }
1805 {
1806 assertion = cfg.provision.dashboards.settings == null || cfg.provision.dashboards.path == null;
1807 message = "Cannot set both dashboards settings and dashboards path";
1808 }
1809 {
1810 assertion = cfg.provision.alerting.rules.settings == null || cfg.provision.alerting.rules.path == null;
1811 message = "Cannot set both rules settings and rules path";
1812 }
1813 {
1814 assertion = cfg.provision.alerting.contactPoints.settings == null || cfg.provision.alerting.contactPoints.path == null;
1815 message = "Cannot set both contact points settings and contact points path";
1816 }
1817 {
1818 assertion = cfg.provision.alerting.policies.settings == null || cfg.provision.alerting.policies.path == null;
1819 message = "Cannot set both policies settings and policies path";
1820 }
1821 {
1822 assertion = cfg.provision.alerting.templates.settings == null || cfg.provision.alerting.templates.path == null;
1823 message = "Cannot set both templates settings and templates path";
1824 }
1825 {
1826 assertion = cfg.provision.alerting.muteTimings.settings == null || cfg.provision.alerting.muteTimings.path == null;
1827 message = "Cannot set both mute timings settings and mute timings path";
1828 }
1829 ];
1830
1831 systemd.services.grafana = {
1832 description = "Grafana Service Daemon";
1833 wantedBy = [ "multi-user.target" ];
1834 after = [ "networking.target" ] ++ lib.optional usePostgresql "postgresql.service" ++ lib.optional useMysql "mysql.service";
1835 script = ''
1836 set -o errexit -o pipefail -o nounset -o errtrace
1837 shopt -s inherit_errexit
1838
1839 exec ${cfg.package}/bin/grafana-server -homepath ${cfg.dataDir} -config ${configFile}
1840 '';
1841 serviceConfig = {
1842 WorkingDirectory = cfg.dataDir;
1843 User = "grafana";
1844 Restart = "on-failure";
1845 RuntimeDirectory = "grafana";
1846 RuntimeDirectoryMode = "0755";
1847 # Hardening
1848 AmbientCapabilities = lib.mkIf (cfg.settings.server.http_port < 1024) [ "CAP_NET_BIND_SERVICE" ];
1849 CapabilityBoundingSet = if (cfg.settings.server.http_port < 1024) then [ "CAP_NET_BIND_SERVICE" ] else [ "" ];
1850 DeviceAllow = [ "" ];
1851 LockPersonality = true;
1852 NoNewPrivileges = true;
1853 PrivateDevices = true;
1854 PrivateTmp = true;
1855 ProtectClock = true;
1856 ProtectControlGroups = true;
1857 ProtectHome = true;
1858 ProtectHostname = true;
1859 ProtectKernelLogs = true;
1860 ProtectKernelModules = true;
1861 ProtectKernelTunables = true;
1862 ProtectProc = "invisible";
1863 ProtectSystem = "full";
1864 RemoveIPC = true;
1865 RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
1866 RestrictNamespaces = true;
1867 RestrictRealtime = true;
1868 RestrictSUIDSGID = true;
1869 SystemCallArchitectures = "native";
1870 # Upstream grafana is not setting SystemCallFilter for compatibility
1871 # reasons, see https://github.com/grafana/grafana/pull/40176
1872 SystemCallFilter = [
1873 "@system-service"
1874 "~@privileged"
1875 ] ++ lib.optionals (cfg.settings.server.protocol == "socket") [ "@chown" ];
1876 UMask = "0027";
1877 };
1878 preStart = ''
1879 ln -fs ${cfg.package}/share/grafana/conf ${cfg.dataDir}
1880 ln -fs ${cfg.package}/share/grafana/tools ${cfg.dataDir}
1881 '';
1882 };
1883
1884 users.users.grafana = {
1885 uid = config.ids.uids.grafana;
1886 description = "Grafana user";
1887 home = cfg.dataDir;
1888 createHome = true;
1889 group = "grafana";
1890 };
1891 users.groups.grafana = { };
1892 };
1893}