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