{ config, lib, pkgs, namespace, ... }: with lib; let name = "grafana"; cfg = config.${namespace}.services.${name}; # --------------------------------------------------------------------------- # Community dashboards — fetched at build time, pinned by hash. # --------------------------------------------------------------------------- communityDashboards = pkgs.linkFarm "grafana-community-dashboards" [ { # Node Exporter Full — https://grafana.com/grafana/dashboards/1860 name = "node-exporter-full.json"; path = pkgs.fetchurl { url = "https://grafana.com/api/dashboards/1860/revisions/latest/download"; sha256 = "sha256-pNgn6xgZBEu6LW0lc0cXX2gRkQ8lg/rer34SPE3yEl4="; }; } { # PostgreSQL Database — https://grafana.com/grafana/dashboards/9628 name = "postgresql.json"; path = pkgs.fetchurl { url = "https://grafana.com/api/dashboards/9628/revisions/latest/download"; sha256 = "sha256-UhusNAZbyt7fJV/DhFUK4FKOmnTpG0R15YO2r+nDnMc="; }; } { # Redis Dashboard for prometheus-redis-exporter 1.x — https://grafana.com/grafana/dashboards/763 name = "redis.json"; path = pkgs.fetchurl { url = "https://grafana.com/api/dashboards/763/revisions/latest/download"; sha256 = "sha256-pThz+zHjcTT9vf8fpUuZK/ejNnH9GwEZVXOY27c9Aw8="; }; } { # MySQL Overview — https://grafana.com/grafana/dashboards/7362 name = "mysql.json"; path = pkgs.fetchurl { url = "https://grafana.com/api/dashboards/7362/revisions/latest/download"; sha256 = "sha256-WW7g60KY20XAdyUpumA0hBrjFC9MQGuGjiJKUhSVBXI="; }; } { # Nextcloud — https://grafana.com/grafana/dashboards/9632 name = "nextcloud.json"; path = pkgs.fetchurl { url = "https://grafana.com/api/dashboards/9632/revisions/latest/download"; sha256 = "sha256-Z28Q/sMg3jxglkszAs83IpL8f4p9loNnTQzjc3S/SAQ="; }; } ]; # --------------------------------------------------------------------------- # Custom dashboards — maintained in this repo under dashboards/ # --------------------------------------------------------------------------- customDashboards = pkgs.linkFarm "grafana-custom-dashboards" [ { name = "nut.json"; path = ./dashboards/nut.json; } { name = "caddy.json"; path = ./dashboards/caddy.json; } { name = "gitea.json"; path = ./dashboards/gitea.json; } { name = "nas-overview.json"; path = ./dashboards/nas-overview.json; } ]; # Minimal .my.cnf for the mysqld exporter. No credentials are needed # because runAsLocalSuperUser = true runs as the mysql OS user, which # MariaDB authenticates via the unix_socket plugin automatically. mysqldExporterCnf = pkgs.writeText "prometheus-mysqld-exporter.cnf" '' [client] user=root socket=/run/mysqld/mysqld.sock ''; giteaPort = config.${namespace}.services.gitea.port; resticPort = config.${namespace}.services.restic.port; nextcloudPort = config.${namespace}.services.nextcloud.port; grafanaConfig = lib.${namespace}.mkModule { inherit config name; description = "grafana"; options = { }; moduleConfig = { services = { prometheus = { enable = true; # bearer_token_file paths (e.g. Gitea metrics key) are SOPS secrets # that only exist at runtime, not in the Nix build sandbox. # "syntax-only" still catches config errors without stat-ing the files. checkConfig = "syntax-only"; exporters = { node = { enable = true; enabledCollectors = [ "filesystem" "diskstats" "meminfo" "cpu" "systemd" "processes" ]; extraFlags = [ "--collector.filesystem.mount-points-exclude=^/(dev|proc|sys|run)($|/)" ]; }; libvirt = { enable = false; openFirewall = true; }; nut = { enable = true; openFirewall = true; passwordPath = config.sops.secrets."jallen-nas/ups_password".path; nutUser = upsUser; }; # PostgreSQL — runs as the local postgres superuser via peer auth # (Unix socket, no password required). postgres = { enable = true; runAsLocalSuperUser = true; }; # Redis — single exporter instance covering all four Redis servers # via the multi-target scrape pattern (/scrape?target=). # The exporter needs AF_INET to reach TCP Redis instances. redis = { enable = true; # No fixed --redis.addr: multi-target mode uses ?target= param. }; # MariaDB — runs as the mysql OS user so it can connect via the # Unix socket without a password (unix_socket auth). mysqld = { enable = true; runAsLocalSuperUser = true; configFile = mysqldExporterCnf; }; # Nextcloud — authenticates with the admin account. # passwordFile must be readable by the prometheus-nextcloud-exporter # user; sops mode 0440 + group keys covers that. nextcloud = { enable = true; url = "http://localhost:${toString nextcloudPort}"; username = "mjallen"; passwordFile = config.sops.secrets."jallen-nas/nextcloud/adminpassword".path; }; }; scrapeConfigs = [ # ── System ────────────────────────────────────────────────────────── { job_name = "node"; static_configs = [ { targets = [ "localhost:${toString config.services.prometheus.exporters.node.port}" ]; } ]; } # ── UPS (NUT) ──────────────────────────────────────────────────────── { job_name = "nut"; static_configs = [ { targets = [ "localhost:${toString config.services.prometheus.exporters.nut.port}" ]; } ]; } # ── Databases ──────────────────────────────────────────────────────── { job_name = "postgres"; static_configs = [ { targets = [ "localhost:${toString config.services.prometheus.exporters.postgres.port}" ]; } ]; } { # Redis multi-target: one exporter, four Redis instances. # The redis_exporter's /scrape?target= endpoint proxies each target # so a single exporter process covers all servers. job_name = "redis"; metrics_path = "/scrape"; static_configs = [ { targets = [ "redis://localhost:6379" # authentik "redis://localhost:6363" # ccache "redis://localhost:6380" # manyfold "redis://localhost:6381" # onlyoffice ]; } ]; relabel_configs = [ { source_labels = [ "__address__" ]; target_label = "__param_target"; } { source_labels = [ "__param_target" ]; target_label = "instance"; } { target_label = "__address__"; replacement = "localhost:${toString config.services.prometheus.exporters.redis.port}"; } ]; } { job_name = "mysqld"; static_configs = [ { targets = [ "localhost:${toString config.services.prometheus.exporters.mysqld.port}" ]; } ]; } # ── Application services ───────────────────────────────────────────── { # Caddy exposes its built-in Prometheus endpoint on port 2019. job_name = "caddy"; static_configs = [ { targets = [ "localhost:2019" ]; } ]; } { # Gitea's /metrics endpoint is protected by a Bearer token. job_name = "gitea"; metrics_path = "/metrics"; bearer_token_file = config.sops.secrets."jallen-nas/gitea/metrics-key".path; static_configs = [ { targets = [ "localhost:${toString giteaPort}" ]; } ]; } { # restic REST server exposes Prometheus metrics at /metrics. job_name = "restic"; metrics_path = "/metrics"; static_configs = [ { targets = [ "localhost:${toString resticPort}" ]; } ]; } { job_name = "nextcloud"; static_configs = [ { targets = [ "localhost:${toString config.services.prometheus.exporters.nextcloud.port}" ]; } ]; } ]; }; grafana = { enable = true; settings = { server = { http_port = cfg.port; http_addr = "0.0.0.0"; }; security = { # Read the secret key from a SOPS-managed file at runtime so it # never appears in the Nix store. The "$__file{}" syntax is # Grafana's built-in file provider. secret_key = "$__file{${config.sops.secrets."jallen-nas/grafana/secret-key".path}}"; }; }; dataDir = "${cfg.configDir}/grafana"; provision = { enable = true; datasources.settings.datasources = [ { name = "Prometheus"; type = "prometheus"; access = "proxy"; url = "http://localhost:${toString config.services.prometheus.port}"; } ]; dashboards.settings.providers = [ { name = "community"; orgId = 1; type = "file"; disableDeletion = true; updateIntervalSeconds = 60; allowUiUpdates = false; options.path = communityDashboards; } { name = "custom"; orgId = 1; type = "file"; disableDeletion = true; updateIntervalSeconds = 60; allowUiUpdates = false; options.path = customDashboards; } ]; }; }; }; # The redis exporter needs AF_INET to reach TCP Redis instances. # The default systemd hardening only allows AF_UNIX. systemd.services.prometheus-redis-exporter.serviceConfig.RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ]; }; }; upsUser = "nas-admin"; in { imports = [ grafanaConfig ]; }