python3Packages.orjson: Disable failing tests on 32 bit
[NixPkgs.git] / nixos / modules / services / monitoring / thanos.nix
blob41462da4ff4c421b7bc47351daaa0a18542433ba
1 { config, lib, pkgs, ... }:
3 with lib;
5 let
6   cfg = config.services.thanos;
8   nullOpt = type: description: mkOption {
9     type = types.nullOr type;
10     default = null;
11     description = lib.mdDoc description;
12   };
14   optionToArgs = opt: v  : optional (v != null)  ''--${opt}="${toString v}"'';
15   flagToArgs   = opt: v  : optional v            "--${opt}";
16   listToArgs   = opt: vs : map               (v: ''--${opt}="${v}"'') vs;
17   attrsToArgs  = opt: kvs: mapAttrsToList (k: v: ''--${opt}=${k}=\"${v}\"'') kvs;
19   mkParamDef = type: default: description: mkParam type (description + ''
21     Defaults to `${toString default}` in Thanos
22     when set to `null`.
23   '');
25   mkParam = type: description: {
26     toArgs = optionToArgs;
27     option = nullOpt type description;
28   };
30   mkFlagParam = description: {
31     toArgs = flagToArgs;
32     option = mkOption {
33       type = types.bool;
34       default = false;
35       description = lib.mdDoc description;
36     };
37   };
39   mkListParam = opt: description: {
40     toArgs = _opt: listToArgs opt;
41     option = mkOption {
42       type = types.listOf types.str;
43       default = [];
44       description = lib.mdDoc description;
45     };
46   };
48   mkAttrsParam = opt: description: {
49     toArgs = _opt: attrsToArgs opt;
50     option = mkOption {
51       type = types.attrsOf types.str;
52       default = {};
53       description = lib.mdDoc description;
54     };
55   };
57   mkStateDirParam = opt: default: description: {
58     toArgs = _opt: stateDir: optionToArgs opt "/var/lib/${stateDir}";
59     option = mkOption {
60       type = types.str;
61       inherit default;
62       description = lib.mdDoc description;
63     };
64   };
66   toYAML = name: attrs: pkgs.runCommand name {
67     preferLocalBuild = true;
68     json = builtins.toFile "${name}.json" (builtins.toJSON attrs);
69     nativeBuildInputs = [ pkgs.remarshal ];
70   } "json2yaml -i $json -o $out";
72   thanos = cmd: "${cfg.package}/bin/thanos ${cmd}" +
73     (let args = cfg.${cmd}.arguments;
74      in optionalString (length args != 0) (" \\\n  " +
75          concatStringsSep " \\\n  " args));
77   argumentsOf = cmd: concatLists (collect isList
78     (flip mapParamsRecursive params.${cmd} (path: param:
79       let opt = concatStringsSep "." path;
80           v = getAttrFromPath path cfg.${cmd};
81       in param.toArgs opt v)));
83   mkArgumentsOption = cmd: mkOption {
84     type = types.listOf types.str;
85     default = argumentsOf cmd;
86     defaultText = literalMD ''
87       calculated from `config.services.thanos.${cmd}`
88     '';
89     description = lib.mdDoc ''
90       Arguments to the `thanos ${cmd}` command.
92       Defaults to a list of arguments formed by converting the structured
93       options of {option}`services.thanos.${cmd}` to a list of arguments.
95       Overriding this option will cause none of the structured options to have
96       any effect. So only set this if you know what you're doing!
97     '';
98   };
100   mapParamsRecursive =
101     let noParam = attr: !(attr ? toArgs && attr ? option);
102     in mapAttrsRecursiveCond noParam;
104   paramsToOptions = mapParamsRecursive (_path: param: param.option);
106   params = {
108     log = {
110       log.level = mkParamDef (types.enum ["debug" "info" "warn" "error" "fatal"]) "info" ''
111         Log filtering level.
112       '';
114       log.format = mkParam types.str ''
115         Log format to use.
116       '';
117     };
119     tracing = cfg: {
120       tracing.config-file = {
121         toArgs = _opt: path: optionToArgs "tracing.config-file" path;
122         option = mkOption {
123           type = with types; nullOr str;
124           default = if cfg.tracing.config == null then null
125                     else toString (toYAML "tracing.yaml" cfg.tracing.config);
126           defaultText = literalExpression ''
127             if config.services.thanos.<cmd>.tracing.config == null then null
128             else toString (toYAML "tracing.yaml" config.services.thanos.<cmd>.tracing.config);
129           '';
130           description = lib.mdDoc ''
131             Path to YAML file that contains tracing configuration.
133             See format details: <https://thanos.io/tracing.md/#configuration>
134           '';
135         };
136       };
138       tracing.config =
139         {
140           toArgs = _opt: _attrs: [];
141           option = nullOpt types.attrs ''
142             Tracing configuration.
144             When not `null` the attribute set gets converted to
145             a YAML file and stored in the Nix store. The option
146             {option}`tracing.config-file` will default to its path.
148             If {option}`tracing.config-file` is set this option has no effect.
150             See format details: <https://thanos.io/tracing.md/#configuration>
151           '';
152         };
153     };
155     common = cfg: params.log // params.tracing cfg // {
157       http-address = mkParamDef types.str "0.0.0.0:10902" ''
158         Listen `host:port` for HTTP endpoints.
159       '';
161       grpc-address = mkParamDef types.str "0.0.0.0:10901" ''
162         Listen `ip:port` address for gRPC endpoints (StoreAPI).
164         Make sure this address is routable from other components.
165       '';
167       grpc-server-tls-cert = mkParam types.str ''
168         TLS Certificate for gRPC server, leave blank to disable TLS
169       '';
171       grpc-server-tls-key = mkParam types.str ''
172         TLS Key for the gRPC server, leave blank to disable TLS
173       '';
175       grpc-server-tls-client-ca = mkParam types.str ''
176         TLS CA to verify clients against.
178         If no client CA is specified, there is no client verification on server side.
179         (tls.NoClientCert)
180       '';
181     };
183     objstore = cfg: {
185       objstore.config-file = {
186         toArgs = _opt: path: optionToArgs "objstore.config-file" path;
187         option = mkOption {
188           type = with types; nullOr str;
189           default = if cfg.objstore.config == null then null
190                     else toString (toYAML "objstore.yaml" cfg.objstore.config);
191           defaultText = literalExpression ''
192             if config.services.thanos.<cmd>.objstore.config == null then null
193             else toString (toYAML "objstore.yaml" config.services.thanos.<cmd>.objstore.config);
194           '';
195           description = lib.mdDoc ''
196             Path to YAML file that contains object store configuration.
198             See format details: <https://thanos.io/storage.md/#configuration>
199           '';
200         };
201       };
203       objstore.config =
204         {
205           toArgs = _opt: _attrs: [];
206           option = nullOpt types.attrs ''
207             Object store configuration.
209             When not `null` the attribute set gets converted to
210             a YAML file and stored in the Nix store. The option
211             {option}`objstore.config-file` will default to its path.
213             If {option}`objstore.config-file` is set this option has no effect.
215             See format details: <https://thanos.io/storage.md/#configuration>
216           '';
217         };
218     };
220     sidecar = params.common cfg.sidecar // params.objstore cfg.sidecar // {
222       prometheus.url = mkParamDef types.str "http://localhost:9090" ''
223         URL at which to reach Prometheus's API.
225         For better performance use local network.
226       '';
228       tsdb.path = {
229         toArgs = optionToArgs;
230         option = mkOption {
231           type = types.str;
232           default = "/var/lib/${config.services.prometheus.stateDir}/data";
233           defaultText = literalExpression ''"/var/lib/''${config.services.prometheus.stateDir}/data"'';
234           description = lib.mdDoc ''
235             Data directory of TSDB.
236           '';
237         };
238       };
240       reloader.config-file = mkParam types.str ''
241         Config file watched by the reloader.
242       '';
244       reloader.config-envsubst-file = mkParam types.str ''
245         Output file for environment variable substituted config file.
246       '';
248       reloader.rule-dirs = mkListParam "reloader.rule-dir" ''
249         Rule directories for the reloader to refresh.
250       '';
252     };
254     store = params.common cfg.store // params.objstore cfg.store // {
256       stateDir = mkStateDirParam "data-dir" "thanos-store" ''
257         Data directory relative to `/var/lib`
258         in which to cache remote blocks.
259       '';
261       index-cache-size = mkParamDef types.str "250MB" ''
262         Maximum size of items held in the index cache.
263       '';
265       chunk-pool-size = mkParamDef types.str "2GB" ''
266         Maximum size of concurrently allocatable bytes for chunks.
267       '';
269       store.grpc.series-sample-limit = mkParamDef types.int 0 ''
270         Maximum amount of samples returned via a single Series call.
272         `0` means no limit.
274         NOTE: for efficiency we take 120 as the number of samples in chunk (it
275         cannot be bigger than that), so the actual number of samples might be
276         lower, even though the maximum could be hit.
277       '';
279       store.grpc.series-max-concurrency = mkParamDef types.int 20 ''
280         Maximum number of concurrent Series calls.
281       '';
283       sync-block-duration = mkParamDef types.str "3m" ''
284         Repeat interval for syncing the blocks between local and remote view.
285       '';
287       block-sync-concurrency = mkParamDef types.int 20 ''
288         Number of goroutines to use when syncing blocks from object storage.
289       '';
291       min-time = mkParamDef types.str "0000-01-01T00:00:00Z" ''
292         Start of time range limit to serve.
294         Thanos Store serves only metrics, which happened later than this
295         value. Option can be a constant time in RFC3339 format or time duration
296         relative to current time, such as -1d or 2h45m. Valid duration units are
297         ms, s, m, h, d, w, y.
298       '';
300       max-time = mkParamDef types.str "9999-12-31T23:59:59Z" ''
301         End of time range limit to serve.
303         Thanos Store serves only blocks, which happened eariler than this
304         value. Option can be a constant time in RFC3339 format or time duration
305         relative to current time, such as -1d or 2h45m. Valid duration units are
306         ms, s, m, h, d, w, y.
307       '';
308     };
310     query = params.common cfg.query // {
312       grpc-client-tls-secure = mkFlagParam ''
313         Use TLS when talking to the gRPC server
314       '';
316       grpc-client-tls-cert = mkParam types.str ''
317         TLS Certificates to use to identify this client to the server
318       '';
320       grpc-client-tls-key = mkParam types.str ''
321         TLS Key for the client's certificate
322       '';
324       grpc-client-tls-ca = mkParam types.str ''
325         TLS CA Certificates to use to verify gRPC servers
326       '';
328       grpc-client-server-name = mkParam types.str ''
329         Server name to verify the hostname on the returned gRPC certificates.
330         See <https://tools.ietf.org/html/rfc4366#section-3.1>
331       '';
333       web.route-prefix = mkParam types.str ''
334         Prefix for API and UI endpoints.
336         This allows thanos UI to be served on a sub-path. This option is
337         analogous to {option}`web.route-prefix` of Promethus.
338       '';
340       web.external-prefix = mkParam types.str ''
341         Static prefix for all HTML links and redirect URLs in the UI query web
342         interface.
344         Actual endpoints are still served on / or the
345         {option}`web.route-prefix`. This allows thanos UI to be served
346         behind a reverse proxy that strips a URL sub-path.
347       '';
349       web.prefix-header = mkParam types.str ''
350         Name of HTTP request header used for dynamic prefixing of UI links and
351         redirects.
353         This option is ignored if the option
354         `web.external-prefix` is set.
356         Security risk: enable this option only if a reverse proxy in front of
357         thanos is resetting the header.
359         The setting `web.prefix-header="X-Forwarded-Prefix"`
360         can be useful, for example, if Thanos UI is served via Traefik reverse
361         proxy with `PathPrefixStrip` option enabled, which
362         sends the stripped prefix value in `X-Forwarded-Prefix`
363         header. This allows thanos UI to be served on a sub-path.
364       '';
366       query.timeout = mkParamDef types.str "2m" ''
367         Maximum time to process query by query node.
368       '';
370       query.max-concurrent = mkParamDef types.int 20 ''
371         Maximum number of queries processed concurrently by query node.
372       '';
374       query.replica-label = mkParam types.str ''
375         Label to treat as a replica indicator along which data is
376         deduplicated.
378         Still you will be able to query without deduplication using
379         `dedup=false` parameter.
380       '';
382       selector-labels = mkAttrsParam "selector-label" ''
383         Query selector labels that will be exposed in info endpoint.
384       '';
386       store.addresses = mkListParam "store" ''
387         Addresses of statically configured store API servers.
389         The scheme may be prefixed with `dns+` or
390         `dnssrv+` to detect store API servers through
391         respective DNS lookups.
392       '';
394       store.sd-files = mkListParam "store.sd-files" ''
395         Path to files that contain addresses of store API servers. The path
396         can be a glob pattern.
397       '';
399       store.sd-interval = mkParamDef types.str "5m" ''
400         Refresh interval to re-read file SD files. It is used as a resync fallback.
401       '';
403       store.sd-dns-interval = mkParamDef types.str "30s" ''
404         Interval between DNS resolutions.
405       '';
407       store.unhealthy-timeout = mkParamDef types.str "5m" ''
408         Timeout before an unhealthy store is cleaned from the store UI page.
409       '';
411       query.auto-downsampling = mkFlagParam ''
412         Enable automatic adjustment (step / 5) to what source of data should
413         be used in store gateways if no
414         `max_source_resolution` param is specified.
415       '';
417       query.partial-response = mkFlagParam ''
418         Enable partial response for queries if no
419         `partial_response` param is specified.
420       '';
422       query.default-evaluation-interval = mkParamDef types.str "1m" ''
423         Set default evaluation interval for sub queries.
424       '';
426       store.response-timeout = mkParamDef types.str "0ms" ''
427         If a Store doesn't send any data in this specified duration then a
428         Store will be ignored and partial data will be returned if it's
429         enabled. `0` disables timeout.
430       '';
431     };
433     rule = params.common cfg.rule // params.objstore cfg.rule // {
435       labels = mkAttrsParam "label" ''
436         Labels to be applied to all generated metrics.
438         Similar to external labels for Prometheus,
439         used to identify ruler and its blocks as unique source.
440       '';
442       stateDir = mkStateDirParam "data-dir" "thanos-rule" ''
443         Data directory relative to `/var/lib`.
444       '';
446       rule-files = mkListParam "rule-file" ''
447         Rule files that should be used by rule manager. Can be in glob format.
448       '';
450       eval-interval = mkParamDef types.str "30s" ''
451         The default evaluation interval to use.
452       '';
454       tsdb.block-duration = mkParamDef types.str "2h" ''
455         Block duration for TSDB block.
456       '';
458       tsdb.retention = mkParamDef types.str "48h" ''
459         Block retention time on local disk.
460       '';
462       alertmanagers.urls = mkListParam "alertmanagers.url" ''
463         Alertmanager replica URLs to push firing alerts.
465         Ruler claims success if push to at least one alertmanager from
466         discovered succeeds. The scheme may be prefixed with
467         `dns+` or `dnssrv+` to detect
468         Alertmanager IPs through respective DNS lookups. The port defaults to
469         `9093` or the SRV record's value. The URL path is
470         used as a prefix for the regular Alertmanager API path.
471       '';
473       alertmanagers.send-timeout = mkParamDef types.str "10s" ''
474         Timeout for sending alerts to alertmanager.
475       '';
477       alert.query-url = mkParam types.str ''
478         The external Thanos Query URL that would be set in all alerts 'Source' field.
479       '';
481       alert.label-drop = mkListParam "alert.label-drop" ''
482         Labels by name to drop before sending to alertmanager.
484         This allows alert to be deduplicated on replica label.
486         Similar Prometheus alert relabelling
487       '';
489       web.route-prefix = mkParam types.str ''
490         Prefix for API and UI endpoints.
492         This allows thanos UI to be served on a sub-path.
494         This option is analogous to `--web.route-prefix` of Promethus.
495       '';
497       web.external-prefix = mkParam types.str ''
498         Static prefix for all HTML links and redirect URLs in the UI query web
499         interface.
501         Actual endpoints are still served on / or the
502         {option}`web.route-prefix`. This allows thanos UI to be served
503         behind a reverse proxy that strips a URL sub-path.
504       '';
506       web.prefix-header = mkParam types.str ''
507         Name of HTTP request header used for dynamic prefixing of UI links and
508         redirects.
510         This option is ignored if the option
511         {option}`web.external-prefix` is set.
513         Security risk: enable this option only if a reverse proxy in front of
514         thanos is resetting the header.
516         The header `X-Forwarded-Prefix` can be useful, for
517         example, if Thanos UI is served via Traefik reverse proxy with
518         `PathPrefixStrip` option enabled, which sends the
519         stripped prefix value in `X-Forwarded-Prefix`
520         header. This allows thanos UI to be served on a sub-path.
521       '';
523       query.addresses = mkListParam "query" ''
524         Addresses of statically configured query API servers.
526         The scheme may be prefixed with `dns+` or
527         `dnssrv+` to detect query API servers through
528         respective DNS lookups.
529       '';
531       query.sd-files = mkListParam "query.sd-files" ''
532         Path to file that contain addresses of query peers.
533         The path can be a glob pattern.
534       '';
536       query.sd-interval = mkParamDef types.str "5m" ''
537         Refresh interval to re-read file SD files. (used as a fallback)
538       '';
540       query.sd-dns-interval = mkParamDef types.str "30s" ''
541         Interval between DNS resolutions.
542       '';
543     };
545     compact = params.log // params.tracing cfg.compact // params.objstore cfg.compact // {
547       http-address = mkParamDef types.str "0.0.0.0:10902" ''
548         Listen `host:port` for HTTP endpoints.
549       '';
551       stateDir = mkStateDirParam "data-dir" "thanos-compact" ''
552         Data directory relative to `/var/lib`
553         in which to cache blocks and process compactions.
554       '';
556       consistency-delay = mkParamDef types.str "30m" ''
557         Minimum age of fresh (non-compacted) blocks before they are being
558         processed. Malformed blocks older than the maximum of consistency-delay
559         and 30m0s will be removed.
560       '';
562       retention.resolution-raw = mkParamDef types.str "0d" ''
563         How long to retain raw samples in bucket.
565         `0d` - disables this retention
566       '';
568       retention.resolution-5m = mkParamDef types.str "0d" ''
569         How long to retain samples of resolution 1 (5 minutes) in bucket.
571         `0d` - disables this retention
572       '';
574       retention.resolution-1h = mkParamDef types.str "0d" ''
575         How long to retain samples of resolution 2 (1 hour) in bucket.
577         `0d` - disables this retention
578       '';
580       startAt = {
581         toArgs = _opt: startAt: flagToArgs "wait" (startAt == null);
582         option = nullOpt types.str ''
583           When this option is set to a `systemd.time`
584           specification the Thanos compactor will run at the specified period.
586           When this option is `null` the Thanos compactor service
587           will run continuously. So it will not exit after all compactions have
588           been processed but wait for new work.
589         '';
590       };
592       downsampling.disable = mkFlagParam ''
593         Disables downsampling.
595         This is not recommended as querying long time ranges without
596         non-downsampled data is not efficient and useful e.g it is not possible
597         to render all samples for a human eye anyway
598       '';
600       block-sync-concurrency = mkParamDef types.int 20 ''
601         Number of goroutines to use when syncing block metadata from object storage.
602       '';
604       compact.concurrency = mkParamDef types.int 1 ''
605         Number of goroutines to use when compacting groups.
606       '';
607     };
609     downsample = params.log // params.tracing cfg.downsample // params.objstore cfg.downsample // {
611       stateDir = mkStateDirParam "data-dir" "thanos-downsample" ''
612         Data directory relative to `/var/lib`
613         in which to cache blocks and process downsamplings.
614       '';
616     };
618     receive = params.common cfg.receive // params.objstore cfg.receive // {
620       remote-write.address = mkParamDef types.str "0.0.0.0:19291" ''
621         Address to listen on for remote write requests.
622       '';
624       stateDir = mkStateDirParam "tsdb.path" "thanos-receive" ''
625         Data directory relative to `/var/lib` of TSDB.
626       '';
628       labels = mkAttrsParam "labels" ''
629         External labels to announce.
631         This flag will be removed in the future when handling multiple tsdb
632         instances is added.
633       '';
635       tsdb.retention = mkParamDef types.str "15d" ''
636         How long to retain raw samples on local storage.
638         `0d` - disables this retention
639       '';
640     };
642   };
644   assertRelativeStateDir = cmd: {
645     assertions = [
646       {
647         assertion = !hasPrefix "/" cfg.${cmd}.stateDir;
648         message =
649           "The option services.thanos.${cmd}.stateDir should not be an absolute directory." +
650           " It should be a directory relative to /var/lib.";
651       }
652     ];
653   };
655 in {
657   options.services.thanos = {
659     package = mkOption {
660       type = types.package;
661       default = pkgs.thanos;
662       defaultText = literalExpression "pkgs.thanos";
663       description = lib.mdDoc ''
664         The thanos package that should be used.
665       '';
666     };
668     sidecar = paramsToOptions params.sidecar // {
669       enable = mkEnableOption
670         (lib.mdDoc "the Thanos sidecar for Prometheus server");
671       arguments = mkArgumentsOption "sidecar";
672     };
674     store = paramsToOptions params.store // {
675       enable = mkEnableOption
676         (lib.mdDoc "the Thanos store node giving access to blocks in a bucket provider.");
677       arguments = mkArgumentsOption "store";
678     };
680     query = paramsToOptions params.query // {
681       enable = mkEnableOption
682         (lib.mdDoc ("the Thanos query node exposing PromQL enabled Query API " +
683          "with data retrieved from multiple store nodes"));
684       arguments = mkArgumentsOption "query";
685     };
687     rule = paramsToOptions params.rule // {
688       enable = mkEnableOption
689         (lib.mdDoc ("the Thanos ruler service which evaluates Prometheus rules against" +
690         " given Query nodes, exposing Store API and storing old blocks in bucket"));
691       arguments = mkArgumentsOption "rule";
692     };
694     compact = paramsToOptions params.compact // {
695       enable = mkEnableOption
696         (lib.mdDoc "the Thanos compactor which continuously compacts blocks in an object store bucket");
697       arguments = mkArgumentsOption "compact";
698     };
700     downsample = paramsToOptions params.downsample // {
701       enable = mkEnableOption
702         (lib.mdDoc "the Thanos downsampler which continuously downsamples blocks in an object store bucket");
703       arguments = mkArgumentsOption "downsample";
704     };
706     receive = paramsToOptions params.receive // {
707       enable = mkEnableOption
708         (lib.mdDoc ("the Thanos receiver which accept Prometheus remote write API requests " +
709          "and write to local tsdb (EXPERIMENTAL, this may change drastically without notice)"));
710       arguments = mkArgumentsOption "receive";
711     };
712   };
714   config = mkMerge [
716     (mkIf cfg.sidecar.enable {
717       assertions = [
718         {
719           assertion = config.services.prometheus.enable;
720           message =
721             "Please enable services.prometheus when enabling services.thanos.sidecar.";
722         }
723         {
724           assertion = !(config.services.prometheus.globalConfig.external_labels == null ||
725                         config.services.prometheus.globalConfig.external_labels == {});
726           message =
727             "services.thanos.sidecar requires uniquely identifying external labels " +
728             "to be configured in the Prometheus server. " +
729             "Please set services.prometheus.globalConfig.external_labels.";
730         }
731       ];
732       systemd.services.thanos-sidecar = {
733         wantedBy = [ "multi-user.target" ];
734         after    = [ "network.target" "prometheus.service" ];
735         serviceConfig = {
736           User = "prometheus";
737           Restart = "always";
738           ExecStart = thanos "sidecar";
739         };
740       };
741     })
743     (mkIf cfg.store.enable (mkMerge [
744       (assertRelativeStateDir "store")
745       {
746         systemd.services.thanos-store = {
747           wantedBy = [ "multi-user.target" ];
748           after    = [ "network.target" ];
749           serviceConfig = {
750             DynamicUser = true;
751             StateDirectory = cfg.store.stateDir;
752             Restart = "always";
753             ExecStart = thanos "store";
754           };
755         };
756       }
757     ]))
759     (mkIf cfg.query.enable {
760       systemd.services.thanos-query = {
761         wantedBy = [ "multi-user.target" ];
762         after    = [ "network.target" ];
763         serviceConfig = {
764           DynamicUser = true;
765           Restart = "always";
766           ExecStart = thanos "query";
767         };
768       };
769     })
771     (mkIf cfg.rule.enable (mkMerge [
772       (assertRelativeStateDir "rule")
773       {
774         systemd.services.thanos-rule = {
775           wantedBy = [ "multi-user.target" ];
776           after    = [ "network.target" ];
777           serviceConfig = {
778             DynamicUser = true;
779             StateDirectory = cfg.rule.stateDir;
780             Restart = "always";
781             ExecStart = thanos "rule";
782           };
783         };
784       }
785     ]))
787     (mkIf cfg.compact.enable (mkMerge [
788       (assertRelativeStateDir "compact")
789       {
790         systemd.services.thanos-compact =
791           let wait = cfg.compact.startAt == null; in {
792             wantedBy = [ "multi-user.target" ];
793             after    = [ "network.target" ];
794             serviceConfig = {
795               Type    = if wait then "simple" else "oneshot";
796               Restart = if wait then "always" else "no";
797               DynamicUser = true;
798               StateDirectory = cfg.compact.stateDir;
799               ExecStart = thanos "compact";
800             };
801           } // optionalAttrs (!wait) { inherit (cfg.compact) startAt; };
802       }
803     ]))
805     (mkIf cfg.downsample.enable (mkMerge [
806       (assertRelativeStateDir "downsample")
807       {
808         systemd.services.thanos-downsample = {
809           wantedBy = [ "multi-user.target" ];
810           after    = [ "network.target" ];
811           serviceConfig = {
812             DynamicUser = true;
813             StateDirectory = cfg.downsample.stateDir;
814             Restart = "always";
815             ExecStart = thanos "downsample";
816           };
817         };
818       }
819     ]))
821     (mkIf cfg.receive.enable (mkMerge [
822       (assertRelativeStateDir "receive")
823       {
824         systemd.services.thanos-receive = {
825           wantedBy = [ "multi-user.target" ];
826           after    = [ "network.target" ];
827           serviceConfig = {
828             DynamicUser = true;
829             StateDirectory = cfg.receive.stateDir;
830             Restart = "always";
831             ExecStart = thanos "receive";
832           };
833         };
834       }
835     ]))
837   ];