···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";
1213- settingsFormatIni = pkgs.formats.ini {};
14 configFile = settingsFormatIni.generate "config.ini" cfg.settings;
1516 mkProvisionCfg = name: attr: provisionCfg:
17 if provisionCfg.path != null
18- then provisionCfg.path
19 else
20 provisioningSettingsFormat.generate "${name}.yaml"
21 (if provisionCfg.settings != null
22- then provisionCfg.settings
23- else {
24- apiVersion = 1;
25- ${attr} = [];
26- });
2728 datasourceFileOrDir = mkProvisionCfg "datasource" "datasources" cfg.provision.datasources;
29 dashboardFileOrDir = mkProvisionCfg "dashboard" "providers" cfg.provision.dashboards;
···3536 notifierFileOrDir = pkgs.writeText "notifier.yaml" (builtins.toJSON notifierConfiguration);
3738- generateAlertingProvisioningYaml = x: if (cfg.provision.alerting."${x}".path == null)
39- then provisioningSettingsFormat.generate "${x}.yaml" cfg.provision.alerting."${x}".settings
40- else cfg.provision.alerting."${x}".path;
041 rulesFileOrDir = generateAlertingProvisioningYaml "rules";
42 contactPointsFileOrDir = generateAlertingProvisioningYaml "contactPoints";
43 policiesFileOrDir = generateAlertingProvisioningYaml "policies";
···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 };
···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 {
···225 };
226 };
227 };
228-in {
0229 imports = [
230 (mkRenamedOptionModule [ "services" "grafana" "protocol" ] [ "services" "grafana" "settings" "server" "protocol" ])
231 (mkRenamedOptionModule [ "services" "grafana" "addr" ] [ "services" "grafana" "settings" "server" "http_addr" ])
···354 protocol = mkOption {
355 description = lib.mdDoc "Which protocol to listen.";
356 default = "http";
357- type = types.enum ["http" "https" "h2" "socket"];
358 };
359360 http_addr = mkOption {
···376 };
377378 domain = mkOption {
379- description = lib.mdDoc "The public facing domain name used to access grafana from a browser.";
00000380 default = "localhost";
381 type = types.str;
000000000382 };
383384 root_url = mkOption {
385- description = lib.mdDoc "Full public facing url.";
0000000386 default = "%(protocol)s://%(domain)s:%(http_port)s/";
387 type = types.str;
388 };
38900000000000000000000000390 static_root_path = mkOption {
391 description = lib.mdDoc "Root path for static assets.";
392 default = "${cfg.package}/share/grafana/public";
···396397 enable_gzip = mkOption {
398 description = lib.mdDoc ''
399- Set this option to true to enable HTTP compression, this can improve transfer speed and bandwidth utilization.
400- It is recommended that most users set it to true. By default it is set to false for compatibility reasons.
401 '';
402 default = false;
403 type = types.bool;
404 };
405406 cert_file = mkOption {
407- description = lib.mdDoc "Cert file for ssl.";
00408 default = "";
409 type = types.str;
410 };
411412 cert_key = mkOption {
413- description = lib.mdDoc "Cert key for ssl.";
00414 default = "";
415 type = types.str;
416 };
41700000000000000000000000418 socket = mkOption {
419- description = lib.mdDoc "Path where the socket should be created when protocol=socket. Make sure that Grafana has appropriate permissions before you change this setting.";
000420 default = "/run/grafana/grafana.sock";
0000000000000000000000421 type = types.str;
422 };
423 };
···426 type = mkOption {
427 description = lib.mdDoc "Database type.";
428 default = "sqlite3";
429- type = types.enum ["mysql" "sqlite3" "postgres"];
430 };
431432 host = mkOption {
433- description = lib.mdDoc "Database host.";
00000434 default = "127.0.0.1:3306";
435 type = types.str;
436 };
437438 name = mkOption {
439- description = lib.mdDoc "Database name.";
440 default = "grafana";
441 type = types.str;
442 };
443444 user = mkOption {
445- description = lib.mdDoc "Database user.";
446 default = "root";
447 type = types.str;
448 };
449450 password = mkOption {
451 description = lib.mdDoc ''
452- Database password. Please note that the contents of this option
00453 will end up in a world-readable Nix store. Use the file provider
454 pointing at a reasonably secured file in the local filesystem
455 to work around that. Look at the documentation for details:
···459 type = types.str;
460 };
4610000000000000000000000000000000000000000000000000000000000000000000000000000000000462 path = mkOption {
463- description = lib.mdDoc "Only applicable to sqlite3 database. The file path where the database will be stored.";
464 default = "${cfg.dataDir}/data/grafana.db";
465 defaultText = literalExpression ''"''${config.${opt.dataDir}}/data/grafana.db"'';
466 type = types.path;
467 };
00000000000000000000000000000000000000000468 };
469470 security = {
000000471 admin_user = mkOption {
472 description = lib.mdDoc "Default admin username.";
473 default = "admin";
···486 type = types.str;
487 };
488000000489 secret_key = mkOption {
490 description = lib.mdDoc ''
491 Secret key used for signing. Please note that the contents of this option
···497 default = "SW2YcwTIb9zpOOhoPsMm";
498 type = types.str;
499 };
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500 };
501502 smtp = {
···505 default = false;
506 type = types.bool;
507 };
0508 host = mkOption {
509 description = lib.mdDoc "Host to connect to.";
510 default = "localhost:25";
511 type = types.str;
512 };
0513 user = mkOption {
514 description = lib.mdDoc "User used for authentication.";
515 default = "";
516 type = types.str;
517 };
0518 password = mkOption {
519 description = lib.mdDoc ''
520 Password used for authentication. Please note that the contents of this option
···526 default = "";
527 type = types.str;
528 };
0000000000000000000529 from_address = mkOption {
530- description = lib.mdDoc "Email address used for sending.";
531 default = "admin@grafana.localhost";
532 type = types.str;
533 };
000000000000534 };
535536 users = {
537 allow_sign_up = mkOption {
538- description = lib.mdDoc "Disable user signup / registration.";
000539 default = false;
540 type = types.bool;
541 };
542543 allow_org_create = mkOption {
544- description = lib.mdDoc "Whether user is allowed to create organizations.";
545 default = false;
546 type = types.bool;
547 };
548549 auto_assign_org = mkOption {
550- description = lib.mdDoc "Whether to automatically assign new users to default org.";
0000551 default = true;
552 type = types.bool;
553 };
5540000000000555 auto_assign_org_role = mkOption {
556- description = lib.mdDoc "Default role new users will be auto assigned.";
00557 default = "Viewer";
558- type = types.enum ["Viewer" "Editor" "Admin"];
0000000000000000000000000000000000000000000000000000000000000000000000000000559 };
560 };
561562- analytics.reporting_enabled = mkOption {
563- description = lib.mdDoc "Whether to allow anonymous usage reporting to stats.grafana.net.";
564- default = true;
565- type = types.bool;
00000000000000000000000000000000000566 };
567 };
568 };
···575 description = lib.mdDoc ''
576 Declaratively provision Grafana's datasources.
577 '';
578- default = {};
579 type = submodule' {
580 options.settings = mkOption {
581 description = lib.mdDoc ''
···595596 datasources = mkOption {
597 description = lib.mdDoc "List of datasources to insert/update.";
598- default = [];
599 type = types.listOf grafanaTypes.datasourceConfig;
600 };
601602 deleteDatasources = mkOption {
603 description = lib.mdDoc "List of datasources that should be deleted from the database.";
604- default = [];
605 type = types.listOf (types.submodule {
606 options.name = mkOption {
607 description = lib.mdDoc "Name of the datasource to delete.";
···650 description = lib.mdDoc ''
651 Declaratively provision Grafana's dashboards.
652 '';
653- default = {};
654 type = submodule' {
655 options.settings = mkOption {
656 description = lib.mdDoc ''
···669670 options.providers = mkOption {
671 description = lib.mdDoc "List of dashboards to insert/update.";
672- default = [];
673 type = types.listOf grafanaTypes.dashboardConfig;
674 };
675 });
···700701 notifiers = mkOption {
702 description = lib.mdDoc "Grafana notifier configuration.";
703- default = [];
704 type = types.listOf grafanaTypes.notifierConfig;
705 apply = x: map _filter x;
706 };
···736737 groups = mkOption {
738 description = lib.mdDoc "List of rule groups to import or update.";
739- default = [];
740 type = types.listOf (types.submodule {
741 freeformType = provisioningSettingsFormat.type;
742···759760 deleteRules = mkOption {
761 description = lib.mdDoc "List of alert rule UIDs that should be deleted.";
762- default = [];
763 type = types.listOf (types.submodule {
764 options.orgId = mkOption {
765 description = lib.mdDoc "Organization ID, default = 1";
···860861 contactPoints = mkOption {
862 description = lib.mdDoc "List of contact points to import or update.";
863- default = [];
864 type = types.listOf (types.submodule {
865 freeformType = provisioningSettingsFormat.type;
866···873874 deleteContactPoints = mkOption {
875 description = lib.mdDoc "List of receivers that should be deleted.";
876- default = [];
877 type = types.listOf (types.submodule {
878 options.orgId = mkOption {
879 description = lib.mdDoc "Organization ID, default = 1.";
···941942 policies = mkOption {
943 description = lib.mdDoc "List of contact points to import or update.";
944- default = [];
945 type = types.listOf (types.submodule {
946 freeformType = provisioningSettingsFormat.type;
947 });
···949950 resetPolicies = mkOption {
951 description = lib.mdDoc "List of orgIds that should be reset to the default policy.";
952- default = [];
953 type = types.listOf types.int;
954 };
955 };
···10111012 templates = mkOption {
1013 description = lib.mdDoc "List of templates to import or update.";
1014- default = [];
1015 type = types.listOf (types.submodule {
1016 freeformType = provisioningSettingsFormat.type;
1017···10291030 deleteTemplates = mkOption {
1031 description = lib.mdDoc "List of alert rule UIDs that should be deleted.";
1032- default = [];
1033 type = types.listOf (types.submodule {
1034 options.orgId = mkOption {
1035 description = lib.mdDoc "Organization ID, default = 1.";
···10931094 muteTimes = mkOption {
1095 description = lib.mdDoc "List of mute time intervals to import or update.";
1096- default = [];
1097 type = types.listOf (types.submodule {
1098 freeformType = provisioningSettingsFormat.type;
1099···11061107 deleteMuteTimes = mkOption {
1108 description = lib.mdDoc "List of mute time intervals that should be deleted.";
1109- default = [];
1110 type = types.listOf (types.submodule {
1111 options.orgId = mkOption {
1112 description = lib.mdDoc "Organization ID, default = 1.";
···1168 };
11691170 config = mkIf cfg.enable {
1171- warnings = let
1172- doesntUseFileProvider = opt: defaultValue:
1173- let
1174- regex = "${optionalString (defaultValue != null) "^${defaultValue}$|"}^\\$__(file|env)\\{.*}$|^\\$[^_\\$][^ ]+$";
1175- in builtins.match regex opt == null;
1176- in
1177- # Ensure that no custom credentials are leaked into the Nix store. Unless the default value
1178- # is specified, this can be achieved by using the file/env provider:
1179- # https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#variable-expansion
1180- (optional (
1181- doesntUseFileProvider cfg.settings.database.password "" ||
1182- doesntUseFileProvider cfg.settings.security.admin_password "admin"
1183- ) ''
1184- Grafana passwords will be stored as plaintext in the Nix store!
1185- Use file provider or an env-var instead.
1186- '')
1187- # Warn about deprecated notifiers.
1188- ++ (optional (cfg.provision.notifiers != []) ''
1189- Notifiers are deprecated upstream and will be removed in Grafana 10.
1190- Use `services.grafana.provision.alerting.contactPoints` instead.
1191- '')
1192- # Ensure that `secureJsonData` of datasources provisioned via `datasources.settings`
1193- # only uses file/env providers.
1194- ++ (optional (
1195- let
1196- datasourcesToCheck = optionals
1197- (cfg.provision.datasources.settings != null)
1198- cfg.provision.datasources.settings.datasources;
1199- declarationUnsafe = { secureJsonData, ... }:
1200- secureJsonData != null
1201- && any (flip doesntUseFileProvider null) (attrValues secureJsonData);
1202- in any declarationUnsafe datasourcesToCheck
1203- ) ''
1204- Declarations in the `secureJsonData`-block of a datasource will be leaked to the
1205- Nix store unless a file-provider or an env-var is used!
1206- '')
1207- ++ (optional (
1208- any (x: x.secure_settings != null) cfg.provision.notifiers
1209- ) "Notifier secure settings will be stored as plaintext in the Nix store! Use file provider instead.");
000000000000012101211 environment.systemPackages = [ cfg.package ];
1212···1216 message = "Cannot set both datasources settings and datasources path";
1217 }
1218 {
1219- assertion = let
1220- prometheusIsNotDirect = opt: all
1221- ({ type, access, ... }: type == "prometheus" -> access != "direct")
1222- opt;
1223- in
01224 cfg.provision.datasources.settings == null || prometheusIsNotDirect cfg.provision.datasources.settings.datasources;
1225 message = "For datasources of type `prometheus`, the `direct` access mode is not supported anymore (since Grafana 9.2.0)";
1226 }
···12521253 systemd.services.grafana = {
1254 description = "Grafana Service Daemon";
1255- wantedBy = ["multi-user.target"];
1256- after = ["networking.target"] ++ lib.optional usePostgresql "postgresql.service" ++ lib.optional useMysql "mysql.service";
1257 script = ''
1258 set -o errexit -o pipefail -o nounset -o errtrace
1259 shopt -s inherit_errexit
···1309 createHome = true;
1310 group = "grafana";
1311 };
1312- users.groups.grafana = {};
1313 };
1314}
···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";
1213+ settingsFormatIni = pkgs.formats.ini { };
14 configFile = settingsFormatIni.generate "config.ini" cfg.settings;
1516 mkProvisionCfg = name: attr: provisionCfg:
17 if provisionCfg.path != null
18+ then provisionCfg.path
19 else
20 provisioningSettingsFormat.generate "${name}.yaml"
21 (if provisionCfg.settings != null
22+ then provisionCfg.settings
23+ else {
24+ apiVersion = 1;
25+ ${attr} = [ ];
26+ });
2728 datasourceFileOrDir = mkProvisionCfg "datasource" "datasources" cfg.provision.datasources;
29 dashboardFileOrDir = mkProvisionCfg "dashboard" "providers" cfg.provision.dashboards;
···3536 notifierFileOrDir = pkgs.writeText "notifier.yaml" (builtins.toJSON notifierConfiguration);
3738+ generateAlertingProvisioningYaml = x:
39+ if (cfg.provision.alerting."${x}".path == null)
40+ then provisioningSettingsFormat.generate "${x}.yaml" cfg.provision.alerting."${x}".settings
41+ else cfg.provision.alerting."${x}".path;
42 rulesFileOrDir = generateAlertingProvisioningYaml "rules";
43 contactPointsFileOrDir = generateAlertingProvisioningYaml "contactPoints";
44 policiesFileOrDir = generateAlertingProvisioningYaml "policies";
···103 description = lib.mdDoc "Datasource type. Required.";
104 };
105 access = mkOption {
106+ type = types.enum [ "proxy" "direct" ];
107 default = "proxy";
108 description = lib.mdDoc "Access mode. proxy or direct (Server or Browser in the UI). Required.";
109 };
···171 description = lib.mdDoc "Notifier name.";
172 };
173 type = mkOption {
174+ type = types.enum [ "dingding" "discord" "email" "googlechat" "hipchat" "kafka" "line" "teams" "opsgenie" "pagerduty" "prometheus-alertmanager" "pushover" "sensu" "sensugo" "slack" "telegram" "threema" "victorops" "webhook" ];
175 description = lib.mdDoc "Notifier type.";
176 };
177 uid = mkOption {
···226 };
227 };
228 };
229+in
230+{
231 imports = [
232 (mkRenamedOptionModule [ "services" "grafana" "protocol" ] [ "services" "grafana" "settings" "server" "protocol" ])
233 (mkRenamedOptionModule [ "services" "grafana" "addr" ] [ "services" "grafana" "settings" "server" "http_addr" ])
···356 protocol = mkOption {
357 description = lib.mdDoc "Which protocol to listen.";
358 default = "http";
359+ type = types.enum [ "http" "https" "h2" "socket" ];
360 };
361362 http_addr = mkOption {
···378 };
379380 domain = mkOption {
381+ description = lib.mdDoc ''
382+ The public facing domain name used to access grafana from a browser.
383+384+ This setting is only used in the default value of the `root_url` setting.
385+ If you set the latter manually, this option does not have to be specified.
386+ '';
387 default = "localhost";
388 type = types.str;
389+ };
390+391+ enforce_domain = mkOption {
392+ description = lib.mdDoc ''
393+ Redirect to correct domain if the host header does not match the domain.
394+ Prevents DNS rebinding attacks.
395+ '';
396+ default = false;
397+ type = types.bool;
398 };
399400 root_url = mkOption {
401+ description = lib.mdDoc ''
402+ This is the full URL used to access Grafana from a web browser.
403+ This is important if you use Google or GitHub OAuth authentication (for the callback URL to be correct).
404+405+ This setting is also important if you have a reverse proxy in front of Grafana that exposes it through a subpath.
406+ In that case add the subpath to the end of this URL setting.
407+ '';
408+ # https://github.com/grafana/grafana/blob/cb7e18938b8eb6860a64b91aaba13a7eb31bc95b/conf/defaults.ini#L54
409 default = "%(protocol)s://%(domain)s:%(http_port)s/";
410 type = types.str;
411 };
412413+ serve_from_sub_path = mkOption {
414+ description = lib.mdDoc ''
415+ Serve Grafana from subpath specified in the `root_url` setting.
416+ By default it is set to `false` for compatibility reasons.
417+418+ By enabling this setting and using a subpath in `root_url` above,
419+ e.g. `root_url = "http://localhost:3000/grafana"`,
420+ Grafana is accessible on `http://localhost:3000/grafana`.
421+ If accessed without subpath, Grafana will redirect to an URL with the subpath.
422+ '';
423+ default = false;
424+ type = types.bool;
425+ };
426+427+ router_logging = mkOption {
428+ description = lib.mdDoc ''
429+ Set to `true` for Grafana to log all HTTP requests (not just errors).
430+ These are logged as Info level events to the Grafana log.
431+ '';
432+ default = false;
433+ type = types.bool;
434+ };
435+436 static_root_path = mkOption {
437 description = lib.mdDoc "Root path for static assets.";
438 default = "${cfg.package}/share/grafana/public";
···442443 enable_gzip = mkOption {
444 description = lib.mdDoc ''
445+ Set this option to `true` to enable HTTP compression, this can improve transfer speed and bandwidth utilization.
446+ It is recommended that most users set it to `true`. By default it is set to `false` for compatibility reasons.
447 '';
448 default = false;
449 type = types.bool;
450 };
451452 cert_file = mkOption {
453+ description = lib.mdDoc ''
454+ Path to the certificate file (if `protocol` is set to `https` or `h2`).
455+ '';
456 default = "";
457 type = types.str;
458 };
459460 cert_key = mkOption {
461+ description = lib.mdDoc ''
462+ Path to the certificate key file (if `protocol` is set to `https` or `h2`).
463+ '';
464 default = "";
465 type = types.str;
466 };
467468+ socket_gid = mkOption {
469+ description = lib.mdDoc ''
470+ GID where the socket should be set when `protocol=socket`.
471+ 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.
472+ It is recommended to set the gid as http server user gid.
473+ Not set when the value is -1.
474+ '';
475+ default = -1;
476+ type = types.int;
477+ };
478+479+ socket_mode = mkOption {
480+ description = lib.mdDoc ''
481+ Mode where the socket should be set when `protocol=socket`.
482+ Make sure that Grafana process is the file owner before you change this setting.
483+ '';
484+ # I assume this value is interpreted as octal literal by grafana.
485+ # If this was an int, people following tutorials or porting their
486+ # old config could stumble across nix not having octal literals.
487+ default = "0660";
488+ type = types.str;
489+ };
490+491 socket = mkOption {
492+ description = lib.mdDoc ''
493+ Path where the socket should be created when `protocol=socket`.
494+ Make sure that Grafana has appropriate permissions before you change this setting.
495+ '';
496 default = "/run/grafana/grafana.sock";
497+ type = types.str;
498+ };
499+500+ cdn_url = mkOption {
501+ description = lib.mdDoc ''
502+ Specify a full HTTP URL address to the root of your Grafana CDN assets.
503+ Grafana will add edition and version paths.
504+505+ For example, given a cdn url like `https://cdn.myserver.com`
506+ grafana will try to load a javascript file from `http://cdn.myserver.com/grafana-oss/7.4.0/public/build/app.<hash>.js`.
507+ '';
508+ default = "";
509+ type = types.str;
510+ };
511+512+ read_timeout = mkOption {
513+ description = lib.mdDoc ''
514+ Sets the maximum time using a duration format (5s/5m/5ms)
515+ before timing out read of an incoming request and closing idle connections.
516+ 0 means there is no timeout for reading the request.
517+ '';
518+ default = "0";
519 type = types.str;
520 };
521 };
···524 type = mkOption {
525 description = lib.mdDoc "Database type.";
526 default = "sqlite3";
527+ type = types.enum [ "mysql" "sqlite3" "postgres" ];
528 };
529530 host = mkOption {
531+ description = lib.mdDoc ''
532+ Only applicable to MySQL or Postgres.
533+ Includes IP or hostname and port or in case of Unix sockets the path to it.
534+ For example, for MySQL running on the same host as Grafana: `host = "127.0.0.1:3306"`
535+ or with Unix sockets: `host = "/var/run/mysqld/mysqld.sock"`
536+ '';
537 default = "127.0.0.1:3306";
538 type = types.str;
539 };
540541 name = mkOption {
542+ description = lib.mdDoc "The name of the Grafana database.";
543 default = "grafana";
544 type = types.str;
545 };
546547 user = mkOption {
548+ description = lib.mdDoc "The database user (not applicable for `sqlite3`).";
549 default = "root";
550 type = types.str;
551 };
552553 password = mkOption {
554 description = lib.mdDoc ''
555+ The database user's password (not applicable for `sqlite3`).
556+557+ Please note that the contents of this option
558 will end up in a world-readable Nix store. Use the file provider
559 pointing at a reasonably secured file in the local filesystem
560 to work around that. Look at the documentation for details:
···564 type = types.str;
565 };
566567+ max_idle_conn = mkOption {
568+ description = lib.mdDoc "The maximum number of connections in the idle connection pool.";
569+ default = 2;
570+ type = types.int;
571+ };
572+573+ max_open_conn = mkOption {
574+ description = lib.mdDoc "The maximum number of open connections to the database.";
575+ default = 0; # https://github.com/grafana/grafana/blob/cb7e18938b8eb6860a64b91aaba13a7eb31bc95b/conf/defaults.ini#L123-L124
576+ type = types.int;
577+ };
578+579+ conn_max_lifetime = mkOption {
580+ description = lib.mdDoc ''
581+ Sets the maximum amount of time a connection may be reused.
582+ The default is 14400 (which means 14400 seconds or 4 hours).
583+ For MySQL, this setting should be shorter than the `wait_timeout` variable.
584+ '';
585+ default = 14400;
586+ type = types.int;
587+ };
588+589+ locking_attempt_timeout_sec = mkOption {
590+ description = lib.mdDoc ''
591+ For `mysql`, if the `migrationLocking` feature toggle is set,
592+ specify the time (in seconds) to wait before failing to lock the database for the migrations.
593+ '';
594+ default = 0;
595+ type = types.int;
596+ };
597+598+ log_queries = mkOption {
599+ description = lib.mdDoc "Set to `true` to log the sql calls and execution times";
600+ default = false;
601+ type = types.bool;
602+ };
603+604+ ssl_mode = mkOption {
605+ description = lib.mdDoc ''
606+ For Postgres, use either `disable`, `require` or `verify-full`.
607+ For MySQL, use either `true`, `false`, or `skip-verify`.
608+ '';
609+ default = "disable"; # https://github.com/grafana/grafana/blob/cb7e18938b8eb6860a64b91aaba13a7eb31bc95b/conf/defaults.ini#L134
610+ type = types.enum [ "disable" "require" "verify-full" "true" "false" "skip-verify" ];
611+ };
612+613+ isolation_level = mkOption {
614+ description = lib.mdDoc ''
615+ Only the MySQL driver supports isolation levels in Grafana.
616+ In case the value is empty, the driver's default isolation level is applied.
617+ '';
618+ default = null;
619+ type = types.nullOr (types.enum [ "READ-UNCOMMITTED" "READ-COMMITTED" "REPEATABLE-READ" "SERIALIZABLE" ]);
620+ };
621+622+ ca_cert_path = mkOption {
623+ description = lib.mdDoc "The path to the CA certificate to use.";
624+ default = "";
625+ type = types.str;
626+ };
627+628+ client_key_path = mkOption {
629+ description = lib.mdDoc "The path to the client key. Only if server requires client authentication.";
630+ default = "";
631+ type = types.str;
632+ };
633+634+ client_cert_path = mkOption {
635+ description = lib.mdDoc "The path to the client cert. Only if server requires client authentication.";
636+ default = "";
637+ type = types.str;
638+ };
639+640+ server_cert_name = mkOption {
641+ description = lib.mdDoc ''
642+ The common name field of the certificate used by the `mysql` or `postgres` server.
643+ Not necessary if `ssl_mode` is set to `skip-verify`.
644+ '';
645+ default = "";
646+ type = types.str;
647+ };
648+649 path = mkOption {
650+ description = lib.mdDoc "Only applicable to `sqlite3` database. The file path where the database will be stored.";
651 default = "${cfg.dataDir}/data/grafana.db";
652 defaultText = literalExpression ''"''${config.${opt.dataDir}}/data/grafana.db"'';
653 type = types.path;
654 };
655+656+ cache_mode = mkOption {
657+ description = lib.mdDoc ''
658+ For `sqlite3` only.
659+ [Shared cache](https://www.sqlite.org/sharedcache.html) setting used for connecting to the database.
660+ '';
661+ default = "private";
662+ type = types.enum [ "private" "shared" ];
663+ };
664+665+ wal = mkOption {
666+ description = lib.mdDoc ''
667+ For `sqlite3` only.
668+ Setting to enable/disable [Write-Ahead Logging](https://sqlite.org/wal.html).
669+ '';
670+ default = false;
671+ type = types.bool;
672+ };
673+674+ query_retries = mkOption {
675+ description = lib.mdDoc ''
676+ This setting applies to `sqlite3` only and controls the number of times the system retries a query when the database is locked.
677+ '';
678+ default = 0;
679+ type = types.int;
680+ };
681+682+ transaction_retries = mkOption {
683+ description = lib.mdDoc ''
684+ This setting applies to `sqlite3` only and controls the number of times the system retries a transaction when the database is locked.
685+ '';
686+ default = 5;
687+ type = types.int;
688+ };
689+690+ # TODO Add "instrument_queries" option when upgrading to grafana 10.0
691+ # instrument_queries = mkOption {
692+ # description = lib.mdDoc "Set to `true` to add metrics and tracing for database queries.";
693+ # default = false;
694+ # type = types.bool;
695+ # };
696 };
697698 security = {
699+ disable_initial_admin_creation = mkOption {
700+ description = lib.mdDoc "Disable creation of admin user on first start of Grafana.";
701+ default = false;
702+ type = types.bool;
703+ };
704+705 admin_user = mkOption {
706 description = lib.mdDoc "Default admin username.";
707 default = "admin";
···720 type = types.str;
721 };
722723+ admin_email = mkOption {
724+ description = lib.mdDoc "The email of the default Grafana Admin, created on startup.";
725+ default = "admin@localhost";
726+ type = types.str;
727+ };
728+729 secret_key = mkOption {
730 description = lib.mdDoc ''
731 Secret key used for signing. Please note that the contents of this option
···737 default = "SW2YcwTIb9zpOOhoPsMm";
738 type = types.str;
739 };
740+741+ disable_gravatar = mkOption {
742+ description = lib.mdDoc "Set to `true` to disable the use of Gravatar for user profile images.";
743+ default = false;
744+ type = types.bool;
745+ };
746+747+ data_source_proxy_whitelist = mkOption {
748+ description = lib.mdDoc ''
749+ Define a whitelist of allowed IP addresses or domains, with ports,
750+ to be used in data source URLs with the Grafana data source proxy.
751+ Format: `ip_or_domain:port` separated by spaces.
752+ PostgreSQL, MySQL, and MSSQL data sources do not use the proxy and are therefore unaffected by this setting.
753+ '';
754+ default = "";
755+ type = types.str;
756+ };
757+758+ disable_brute_force_login_protection = mkOption {
759+ description = lib.mdDoc "Set to `true` to disable [brute force login protection](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#account-lockout).";
760+ default = false;
761+ type = types.bool;
762+ };
763+764+ cookie_secure = mkOption {
765+ description = lib.mdDoc "Set to `true` if you host Grafana behind HTTPS.";
766+ default = false;
767+ type = types.bool;
768+ };
769+770+ cookie_samesite = mkOption {
771+ description = lib.mdDoc ''
772+ Sets the `SameSite` cookie attribute and prevents the browser from sending this cookie along with cross-site requests.
773+ The main goal is to mitigate the risk of cross-origin information leakage.
774+ This setting also provides some protection against cross-site request forgery attacks (CSRF),
775+ [read more about SameSite here](https://owasp.org/www-community/SameSite).
776+ Using value `disabled` does not add any `SameSite` attribute to cookies.
777+ '';
778+ default = "lax";
779+ type = types.enum [ "lax" "strict" "none" "disabled" ];
780+ };
781+782+ allow_embedding = mkOption {
783+ description = lib.mdDoc ''
784+ When `false`, the HTTP header `X-Frame-Options: deny` will be set in Grafana HTTP responses
785+ which will instruct browsers to not allow rendering Grafana in a `<frame>`, `<iframe>`, `<embed>` or `<object>`.
786+ The main goal is to mitigate the risk of [Clickjacking](https://owasp.org/www-community/attacks/Clickjacking).
787+ '';
788+ default = false;
789+ type = types.bool;
790+ };
791+792+ strict_transport_security = mkOption {
793+ description = lib.mdDoc ''
794+ Set to `true` if you want to enable HTTP `Strict-Transport-Security` (HSTS) response header.
795+ Only use this when HTTPS is enabled in your configuration,
796+ or when there is another upstream system that ensures your application does HTTPS (like a frontend load balancer).
797+ HSTS tells browsers that the site should only be accessed using HTTPS.
798+ '';
799+ default = false;
800+ type = types.bool;
801+ };
802+803+ strict_transport_security_max_age_seconds = mkOption {
804+ description = lib.mdDoc ''
805+ Sets how long a browser should cache HSTS in seconds.
806+ Only applied if `strict_transport_security` is enabled.
807+ '';
808+ default = 86400;
809+ type = types.int;
810+ };
811+812+ strict_transport_security_preload = mkOption {
813+ description = lib.mdDoc ''
814+ Set to `true` to enable HSTS `preloading` option.
815+ Only applied if `strict_transport_security` is enabled.
816+ '';
817+ default = false;
818+ type = types.bool;
819+ };
820+821+ strict_transport_security_subdomains = mkOption {
822+ description = lib.mdDoc ''
823+ Set to `true` to enable HSTS `includeSubDomains` option.
824+ Only applied if `strict_transport_security` is enabled.
825+ '';
826+ default = false;
827+ type = types.bool;
828+ };
829+830+ x_content_type_options = mkOption {
831+ description = lib.mdDoc ''
832+ Set to `false` to disable the `X-Content-Type-Options` response header.
833+ The `X-Content-Type-Options` response HTTP header is a marker used by the server
834+ to indicate that the MIME types advertised in the `Content-Type` headers should not be changed and be followed.
835+ '';
836+ default = true;
837+ type = types.bool;
838+ };
839+840+ x_xss_protection = mkOption {
841+ description = lib.mdDoc ''
842+ Set to `false` to disable the `X-XSS-Protection` header,
843+ which tells browsers to stop pages from loading when they detect reflected cross-site scripting (XSS) attacks.
844+ '';
845+ default = true;
846+ type = types.bool;
847+ };
848+849+ content_security_policy = mkOption {
850+ description = lib.mdDoc ''
851+ Set to `true` to add the `Content-Security-Policy` header to your requests.
852+ CSP allows to control resources that the user agent can load and helps prevent XSS attacks.
853+ '';
854+ default = false;
855+ type = types.bool;
856+ };
857+858+ content_security_policy_report_only = mkOption {
859+ description = lib.mdDoc ''
860+ Set to `true` to add the `Content-Security-Policy-Report-Only` header to your requests.
861+ CSP in Report Only mode enables you to experiment with policies by monitoring their effects without enforcing them.
862+ You can enable both policies simultaneously.
863+ '';
864+ default = false;
865+ type = types.bool;
866+ };
867+868+ # The options content_security_policy_template and
869+ # content_security_policy_template are missing because I'm not sure
870+ # how exactly the quoting of the default value works. See also
871+ # https://github.com/grafana/grafana/blob/cb7e18938b8eb6860a64b91aaba13a7eb31bc95b/conf/defaults.ini#L364
872+ # https://github.com/grafana/grafana/blob/cb7e18938b8eb6860a64b91aaba13a7eb31bc95b/conf/defaults.ini#L373
873 };
874875 smtp = {
···878 default = false;
879 type = types.bool;
880 };
881+882 host = mkOption {
883 description = lib.mdDoc "Host to connect to.";
884 default = "localhost:25";
885 type = types.str;
886 };
887+888 user = mkOption {
889 description = lib.mdDoc "User used for authentication.";
890 default = "";
891 type = types.str;
892 };
893+894 password = mkOption {
895 description = lib.mdDoc ''
896 Password used for authentication. Please note that the contents of this option
···902 default = "";
903 type = types.str;
904 };
905+906+ cert_file = mkOption {
907+ description = lib.mdDoc "File path to a cert file.";
908+ default = "";
909+ type = types.str;
910+ };
911+912+ key_file = mkOption {
913+ description = lib.mdDoc "File path to a key file.";
914+ default = "";
915+ type = types.str;
916+ };
917+918+ skip_verify = mkOption {
919+ description = lib.mdDoc "Verify SSL for SMTP server.";
920+ default = false;
921+ type = types.bool;
922+ };
923+924 from_address = mkOption {
925+ description = lib.mdDoc "Address used when sending out emails.";
926 default = "admin@grafana.localhost";
927 type = types.str;
928 };
929+930+ from_name = mkOption {
931+ description = lib.mdDoc "Name to be used as client identity for EHLO in SMTP dialog.";
932+ default = "Grafana";
933+ type = types.str;
934+ };
935+936+ startTLS_policy = mkOption {
937+ description = lib.mdDoc "StartTLS policy when connecting to server.";
938+ default = null;
939+ type = types.nullOr (types.enum [ "OpportunisticStartTLS" "MandatoryStartTLS" "NoStartTLS" ]);
940+ };
941 };
942943 users = {
944 allow_sign_up = mkOption {
945+ description = lib.mdDoc ''
946+ Set to false to prohibit users from being able to sign up / create user accounts.
947+ The admin user can still create users.
948+ '';
949 default = false;
950 type = types.bool;
951 };
952953 allow_org_create = mkOption {
954+ description = lib.mdDoc "Set to `false` to prohibit users from creating new organizations.";
955 default = false;
956 type = types.bool;
957 };
958959 auto_assign_org = mkOption {
960+ description = lib.mdDoc ''
961+ Set to `true` to automatically add new users to the main organization (id 1).
962+ When set to `false,` new users automatically cause a new organization to be created for that new user.
963+ The organization will be created even if the `allow_org_create` setting is set to `false`.
964+ '';
965 default = true;
966 type = types.bool;
967 };
968969+ auto_assign_org_id = mkOption {
970+ description = lib.mdDoc ''
971+ Set this value to automatically add new users to the provided org.
972+ This requires `auto_assign_org` to be set to `true`.
973+ Please make sure that this organization already exists.
974+ '';
975+ default = 1;
976+ type = types.int;
977+ };
978+979 auto_assign_org_role = mkOption {
980+ description = lib.mdDoc ''
981+ The role new users will be assigned for the main organization (if the `auto_assign_org` setting is set to `true`).
982+ '';
983 default = "Viewer";
984+ type = types.enum [ "Viewer" "Editor" "Admin" ];
985+ };
986+987+ verify_email_enabled = mkOption {
988+ description = lib.mdDoc "Require email validation before sign up completes.";
989+ default = false;
990+ type = types.bool;
991+ };
992+993+ login_hint = mkOption {
994+ description = lib.mdDoc "Text used as placeholder text on login page for login/username input.";
995+ default = "email or username";
996+ type = types.str;
997+ };
998+999+ password_hint = mkOption {
1000+ description = lib.mdDoc "Text used as placeholder text on login page for password input.";
1001+ default = "password";
1002+ type = types.str;
1003+ };
1004+1005+ default_theme = mkOption {
1006+ description = lib.mdDoc "Sets the default UI theme. `system` matches the user's system theme.";
1007+ default = "dark";
1008+ type = types.enum [ "dark" "light" "system" ];
1009+ };
1010+1011+ default_language = mkOption {
1012+ description = lib.mdDoc "This setting configures the default UI language, which must be a supported IETF language tag, such as `en-US`.";
1013+ default = "en-US";
1014+ type = types.str;
1015+ };
1016+1017+ home_page = mkOption {
1018+ description = lib.mdDoc ''
1019+ Path to a custom home page.
1020+ Users are only redirected to this if the default home dashboard is used.
1021+ It should match a frontend route and contain a leading slash.
1022+ '';
1023+ default = "";
1024+ type = types.str;
1025+ };
1026+1027+ viewers_can_edit = mkOption {
1028+ description = lib.mdDoc ''
1029+ Viewers can access and use Explore and perform temporary edits on panels in dashboards they have access to.
1030+ They cannot save their changes.
1031+ '';
1032+ default = false;
1033+ type = types.bool;
1034+ };
1035+1036+ editors_can_admin = mkOption {
1037+ description = lib.mdDoc "Editors can administrate dashboards, folders and teams they create.";
1038+ default = false;
1039+ type = types.bool;
1040+ };
1041+1042+ user_invite_max_lifetime_duration = mkOption {
1043+ description = lib.mdDoc ''
1044+ The duration in time a user invitation remains valid before expiring.
1045+ This setting should be expressed as a duration.
1046+ Examples: `6h` (hours), `2d` (days), `1w` (week).
1047+ The minimum supported duration is `15m` (15 minutes).
1048+ '';
1049+ default = "24h";
1050+ type = types.str;
1051+ };
1052+1053+ hidden_users = mkOption {
1054+ description = lib.mdDoc ''
1055+ This is a comma-separated list of usernames.
1056+ Users specified here are hidden in the Grafana UI.
1057+ They are still visible to Grafana administrators and to themselves.
1058+ '';
1059+ default = "";
1060+ type = types.str;
1061 };
1062 };
10631064+ analytics = {
1065+ reporting_enabled = mkOption {
1066+ description = lib.mdDoc ''
1067+ When enabled Grafana will send anonymous usage statistics to `stats.grafana.org`.
1068+ No IP addresses are being tracked, only simple counters to track running instances, versions, dashboard and error counts.
1069+ Counters are sent every 24 hours.
1070+ '';
1071+ default = true;
1072+ type = types.bool;
1073+ };
1074+1075+ check_for_updates = mkOption {
1076+ description = lib.mdDoc ''
1077+ When set to `false`, disables checking for new versions of Grafana from Grafana's GitHub repository.
1078+ When enabled, the check for a new version runs every 10 minutes.
1079+ It will notify, via the UI, when a new version is available.
1080+ The check itself will not prompt any auto-updates of the Grafana software, nor will it send any sensitive information.
1081+ '';
1082+ default = true;
1083+ type = types.bool;
1084+ };
1085+1086+ check_for_plugin_updates = mkOption {
1087+ description = lib.mdDoc ''
1088+ When set to `false`, disables checking for new versions of installed plugins from https://grafana.com.
1089+ When enabled, the check for a new plugin runs every 10 minutes.
1090+ It will notify, via the UI, when a new plugin update exists.
1091+ The check itself will not prompt any auto-updates of the plugin, nor will it send any sensitive information.
1092+ '';
1093+ default = cfg.declarativePlugins == null;
1094+ defaultText = literalExpression "cfg.declarativePlugins == null";
1095+ type = types.bool;
1096+ };
1097+1098+ feedback_links_enabled = mkOption {
1099+ description = lib.mdDoc "Set to `false` to remove all feedback links from the UI.";
1100+ default = true;
1101+ type = types.bool;
1102+ };
1103 };
1104 };
1105 };
···1112 description = lib.mdDoc ''
1113 Declaratively provision Grafana's datasources.
1114 '';
1115+ default = { };
1116 type = submodule' {
1117 options.settings = mkOption {
1118 description = lib.mdDoc ''
···11321133 datasources = mkOption {
1134 description = lib.mdDoc "List of datasources to insert/update.";
1135+ default = [ ];
1136 type = types.listOf grafanaTypes.datasourceConfig;
1137 };
11381139 deleteDatasources = mkOption {
1140 description = lib.mdDoc "List of datasources that should be deleted from the database.";
1141+ default = [ ];
1142 type = types.listOf (types.submodule {
1143 options.name = mkOption {
1144 description = lib.mdDoc "Name of the datasource to delete.";
···1187 description = lib.mdDoc ''
1188 Declaratively provision Grafana's dashboards.
1189 '';
1190+ default = { };
1191 type = submodule' {
1192 options.settings = mkOption {
1193 description = lib.mdDoc ''
···12061207 options.providers = mkOption {
1208 description = lib.mdDoc "List of dashboards to insert/update.";
1209+ default = [ ];
1210 type = types.listOf grafanaTypes.dashboardConfig;
1211 };
1212 });
···12371238 notifiers = mkOption {
1239 description = lib.mdDoc "Grafana notifier configuration.";
1240+ default = [ ];
1241 type = types.listOf grafanaTypes.notifierConfig;
1242 apply = x: map _filter x;
1243 };
···12731274 groups = mkOption {
1275 description = lib.mdDoc "List of rule groups to import or update.";
1276+ default = [ ];
1277 type = types.listOf (types.submodule {
1278 freeformType = provisioningSettingsFormat.type;
1279···12961297 deleteRules = mkOption {
1298 description = lib.mdDoc "List of alert rule UIDs that should be deleted.";
1299+ default = [ ];
1300 type = types.listOf (types.submodule {
1301 options.orgId = mkOption {
1302 description = lib.mdDoc "Organization ID, default = 1";
···13971398 contactPoints = mkOption {
1399 description = lib.mdDoc "List of contact points to import or update.";
1400+ default = [ ];
1401 type = types.listOf (types.submodule {
1402 freeformType = provisioningSettingsFormat.type;
1403···14101411 deleteContactPoints = mkOption {
1412 description = lib.mdDoc "List of receivers that should be deleted.";
1413+ default = [ ];
1414 type = types.listOf (types.submodule {
1415 options.orgId = mkOption {
1416 description = lib.mdDoc "Organization ID, default = 1.";
···14781479 policies = mkOption {
1480 description = lib.mdDoc "List of contact points to import or update.";
1481+ default = [ ];
1482 type = types.listOf (types.submodule {
1483 freeformType = provisioningSettingsFormat.type;
1484 });
···14861487 resetPolicies = mkOption {
1488 description = lib.mdDoc "List of orgIds that should be reset to the default policy.";
1489+ default = [ ];
1490 type = types.listOf types.int;
1491 };
1492 };
···15481549 templates = mkOption {
1550 description = lib.mdDoc "List of templates to import or update.";
1551+ default = [ ];
1552 type = types.listOf (types.submodule {
1553 freeformType = provisioningSettingsFormat.type;
1554···15661567 deleteTemplates = mkOption {
1568 description = lib.mdDoc "List of alert rule UIDs that should be deleted.";
1569+ default = [ ];
1570 type = types.listOf (types.submodule {
1571 options.orgId = mkOption {
1572 description = lib.mdDoc "Organization ID, default = 1.";
···16301631 muteTimes = mkOption {
1632 description = lib.mdDoc "List of mute time intervals to import or update.";
1633+ default = [ ];
1634 type = types.listOf (types.submodule {
1635 freeformType = provisioningSettingsFormat.type;
1636···16431644 deleteMuteTimes = mkOption {
1645 description = lib.mdDoc "List of mute time intervals that should be deleted.";
1646+ default = [ ];
1647 type = types.listOf (types.submodule {
1648 options.orgId = mkOption {
1649 description = lib.mdDoc "Organization ID, default = 1.";
···1705 };
17061707 config = mkIf cfg.enable {
1708+ warnings =
1709+ let
1710+ doesntUseFileProvider = opt: defaultValue:
1711+ let regex = "${optionalString (defaultValue != null) "^${defaultValue}$|"}^\\$__(file|env)\\{.*}$|^\\$[^_\\$][^ ]+$";
1712+ in builtins.match regex opt == null;
1713+1714+ # Ensure that no custom credentials are leaked into the Nix store. Unless the default value
1715+ # is specified, this can be achieved by using the file/env provider:
1716+ # https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#variable-expansion
1717+ passwordWithoutFileProvider = optional
1718+ (
1719+ doesntUseFileProvider cfg.settings.database.password "" ||
1720+ doesntUseFileProvider cfg.settings.security.admin_password "admin"
1721+ )
1722+ ''
1723+ Grafana passwords will be stored as plaintext in the Nix store!
1724+ Use file provider or an env-var instead.
1725+ '';
1726+1727+ # Warn about deprecated notifiers.
1728+ deprecatedNotifiers = optional (cfg.provision.notifiers != [ ]) ''
1729+ Notifiers are deprecated upstream and will be removed in Grafana 10.
1730+ Use `services.grafana.provision.alerting.contactPoints` instead.
1731+ '';
1732+1733+ # Ensure that `secureJsonData` of datasources provisioned via `datasources.settings`
1734+ # only uses file/env providers.
1735+ secureJsonDataWithoutFileProvider = optional
1736+ (
1737+ let
1738+ datasourcesToCheck = optionals
1739+ (cfg.provision.datasources.settings != null)
1740+ cfg.provision.datasources.settings.datasources;
1741+ declarationUnsafe = { secureJsonData, ... }:
1742+ secureJsonData != null
1743+ && any (flip doesntUseFileProvider null) (attrValues secureJsonData);
1744+ in
1745+ any declarationUnsafe datasourcesToCheck
1746+ )
1747+ ''
1748+ Declarations in the `secureJsonData`-block of a datasource will be leaked to the
1749+ Nix store unless a file-provider or an env-var is used!
1750+ '';
1751+1752+ notifierSecureSettingsWithoutFileProvider = optional
1753+ (any (x: x.secure_settings != null) cfg.provision.notifiers)
1754+ "Notifier secure settings will be stored as plaintext in the Nix store! Use file provider instead.";
1755+ in
1756+ passwordWithoutFileProvider
1757+ ++ deprecatedNotifiers
1758+ ++ secureJsonDataWithoutFileProvider
1759+ ++ notifierSecureSettingsWithoutFileProvider;
17601761 environment.systemPackages = [ cfg.package ];
1762···1766 message = "Cannot set both datasources settings and datasources path";
1767 }
1768 {
1769+ assertion =
1770+ let
1771+ prometheusIsNotDirect = opt: all
1772+ ({ type, access, ... }: type == "prometheus" -> access != "direct")
1773+ opt;
1774+ in
1775 cfg.provision.datasources.settings == null || prometheusIsNotDirect cfg.provision.datasources.settings.datasources;
1776 message = "For datasources of type `prometheus`, the `direct` access mode is not supported anymore (since Grafana 9.2.0)";
1777 }
···18031804 systemd.services.grafana = {
1805 description = "Grafana Service Daemon";
1806+ wantedBy = [ "multi-user.target" ];
1807+ after = [ "networking.target" ] ++ lib.optional usePostgresql "postgresql.service" ++ lib.optional useMysql "mysql.service";
1808 script = ''
1809 set -o errexit -o pipefail -o nounset -o errtrace
1810 shopt -s inherit_errexit
···1860 createHome = true;
1861 group = "grafana";
1862 };
1863+ users.groups.grafana = { };
1864 };
1865}