1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.graphite;
7 writeTextOrNull = f: t: mapNullable (pkgs.writeTextDir f) t;
8
9 dataDir = cfg.dataDir;
10 staticDir = cfg.dataDir + "/static";
11
12 graphiteLocalSettingsDir = pkgs.runCommand "graphite_local_settings"
13 {inherit graphiteLocalSettings;} ''
14 mkdir -p $out
15 ln -s $graphiteLocalSettings $out/graphite_local_settings.py
16 '';
17
18 graphiteLocalSettings = pkgs.writeText "graphite_local_settings.py" (
19 "STATIC_ROOT = '${staticDir}'\n" +
20 optionalString (! isNull config.time.timeZone) "TIME_ZONE = '${config.time.timeZone}'\n"
21 + cfg.web.extraConfig
22 );
23
24 graphiteApiConfig = pkgs.writeText "graphite-api.yaml" ''
25 search_index: ${dataDir}/index
26 ${optionalString (!isNull config.time.timeZone) ''time_zone: ${config.time.timeZone}''}
27 ${optionalString (cfg.api.finders != []) ''finders:''}
28 ${concatMapStringsSep "\n" (f: " - " + f.moduleName) cfg.api.finders}
29 ${optionalString (cfg.api.functions != []) ''functions:''}
30 ${concatMapStringsSep "\n" (f: " - " + f) cfg.api.functions}
31 ${cfg.api.extraConfig}
32 '';
33
34 seyrenConfig = {
35 SEYREN_URL = cfg.seyren.seyrenUrl;
36 MONGO_URL = cfg.seyren.mongoUrl;
37 GRAPHITE_URL = cfg.seyren.graphiteUrl;
38 } // cfg.seyren.extraConfig;
39
40 pagerConfig = pkgs.writeText "alarms.yaml" cfg.pager.alerts;
41
42 configDir = pkgs.buildEnv {
43 name = "graphite-config";
44 paths = lists.filter (el: el != null) [
45 (writeTextOrNull "carbon.conf" cfg.carbon.config)
46 (writeTextOrNull "storage-aggregation.conf" cfg.carbon.storageAggregation)
47 (writeTextOrNull "storage-schemas.conf" cfg.carbon.storageSchemas)
48 (writeTextOrNull "blacklist.conf" cfg.carbon.blacklist)
49 (writeTextOrNull "whitelist.conf" cfg.carbon.whitelist)
50 (writeTextOrNull "rewrite-rules.conf" cfg.carbon.rewriteRules)
51 (writeTextOrNull "relay-rules.conf" cfg.carbon.relayRules)
52 (writeTextOrNull "aggregation-rules.conf" cfg.carbon.aggregationRules)
53 ];
54 };
55
56 carbonOpts = name: with config.ids; ''
57 --nodaemon --syslog --prefix=${name} --pidfile /run/${name}/${name}.pid ${name}
58 '';
59
60 mkPidFileDir = name: ''
61 mkdir -p /run/${name}
62 chmod 0700 /run/${name}
63 chown -R graphite:graphite /run/${name}
64 '';
65
66 carbonEnv = {
67 PYTHONPATH = let
68 cenv = pkgs.python.buildEnv.override {
69 extraLibs = [ pkgs.python27Packages.carbon ];
70 };
71 cenvPack = "${cenv}/${pkgs.python.sitePackages}";
72 # opt/graphite/lib contains twisted.plugins.carbon-cache
73 in "${cenvPack}/opt/graphite/lib:${cenvPack}";
74 GRAPHITE_ROOT = dataDir;
75 GRAPHITE_CONF_DIR = configDir;
76 GRAPHITE_STORAGE_DIR = dataDir;
77 };
78
79in {
80
81 ###### interface
82
83 options.services.graphite = {
84 dataDir = mkOption {
85 type = types.path;
86 default = "/var/db/graphite";
87 description = ''
88 Data directory for graphite.
89 '';
90 };
91
92 web = {
93 enable = mkOption {
94 description = "Whether to enable graphite web frontend.";
95 default = false;
96 type = types.bool;
97 };
98
99 listenAddress = mkOption {
100 description = "Graphite web frontend listen address.";
101 default = "127.0.0.1";
102 type = types.str;
103 };
104
105 port = mkOption {
106 description = "Graphite web frontend port.";
107 default = 8080;
108 type = types.int;
109 };
110
111 extraConfig = mkOption {
112 type = types.str;
113 default = "";
114 description = ''
115 Graphite webapp settings. See:
116 <link xlink:href="http://graphite.readthedocs.io/en/latest/config-local-settings.html"/>
117 '';
118 };
119 };
120
121 api = {
122 enable = mkOption {
123 description = ''
124 Whether to enable graphite api. Graphite api is lightweight alternative
125 to graphite web, with api and without dashboard. It's advised to use
126 grafana as alternative dashboard and influxdb as alternative to
127 graphite carbon.
128
129 For more information visit
130 <link xlink:href="http://graphite-api.readthedocs.org/en/latest/"/>
131 '';
132 default = false;
133 type = types.bool;
134 };
135
136 finders = mkOption {
137 description = "List of finder plugins to load.";
138 default = [];
139 example = literalExample "[ pkgs.python27Packages.graphite_influxdb ]";
140 type = types.listOf types.package;
141 };
142
143 functions = mkOption {
144 description = "List of functions to load.";
145 default = [
146 "graphite_api.functions.SeriesFunctions"
147 "graphite_api.functions.PieFunctions"
148 ];
149 type = types.listOf types.str;
150 };
151
152 listenAddress = mkOption {
153 description = "Graphite web service listen address.";
154 default = "127.0.0.1";
155 type = types.str;
156 };
157
158 port = mkOption {
159 description = "Graphite api service port.";
160 default = 8080;
161 type = types.int;
162 };
163
164 package = mkOption {
165 description = "Package to use for graphite api.";
166 default = pkgs.python27Packages.graphite_api;
167 defaultText = "pkgs.python27Packages.graphite_api";
168 type = types.package;
169 };
170
171 extraConfig = mkOption {
172 description = "Extra configuration for graphite api.";
173 default = ''
174 whisper:
175 directories:
176 - ${dataDir}/whisper
177 '';
178 example = ''
179 allowed_origins:
180 - dashboard.example.com
181 cheat_times: true
182 influxdb:
183 host: localhost
184 port: 8086
185 user: influxdb
186 pass: influxdb
187 db: metrics
188 cache:
189 CACHE_TYPE: 'filesystem'
190 CACHE_DIR: '/tmp/graphite-api-cache'
191 '';
192 type = types.lines;
193 };
194 };
195
196 carbon = {
197 config = mkOption {
198 description = "Content of carbon configuration file.";
199 default = ''
200 [cache]
201 # Listen on localhost by default for security reasons
202 UDP_RECEIVER_INTERFACE = 127.0.0.1
203 PICKLE_RECEIVER_INTERFACE = 127.0.0.1
204 LINE_RECEIVER_INTERFACE = 127.0.0.1
205 CACHE_QUERY_INTERFACE = 127.0.0.1
206 # Do not log every update
207 LOG_UPDATES = False
208 LOG_CACHE_HITS = False
209 '';
210 type = types.str;
211 };
212
213 enableCache = mkOption {
214 description = "Whether to enable carbon cache, the graphite storage daemon.";
215 default = false;
216 type = types.bool;
217 };
218
219 storageAggregation = mkOption {
220 description = "Defines how to aggregate data to lower-precision retentions.";
221 default = null;
222 type = types.uniq (types.nullOr types.string);
223 example = ''
224 [all_min]
225 pattern = \.min$
226 xFilesFactor = 0.1
227 aggregationMethod = min
228 '';
229 };
230
231 storageSchemas = mkOption {
232 description = "Defines retention rates for storing metrics.";
233 default = "";
234 type = types.uniq (types.nullOr types.string);
235 example = ''
236 [apache_busyWorkers]
237 pattern = ^servers\.www.*\.workers\.busyWorkers$
238 retentions = 15s:7d,1m:21d,15m:5y
239 '';
240 };
241
242 blacklist = mkOption {
243 description = "Any metrics received which match one of the experssions will be dropped.";
244 default = null;
245 type = types.uniq (types.nullOr types.string);
246 example = "^some\.noisy\.metric\.prefix\..*";
247 };
248
249 whitelist = mkOption {
250 description = "Only metrics received which match one of the experssions will be persisted.";
251 default = null;
252 type = types.uniq (types.nullOr types.string);
253 example = ".*";
254 };
255
256 rewriteRules = mkOption {
257 description = ''
258 Regular expression patterns that can be used to rewrite metric names
259 in a search and replace fashion.
260 '';
261 default = null;
262 type = types.uniq (types.nullOr types.string);
263 example = ''
264 [post]
265 _sum$ =
266 _avg$ =
267 '';
268 };
269
270 enableRelay = mkOption {
271 description = "Whether to enable carbon relay, the carbon replication and sharding service.";
272 default = false;
273 type = types.bool;
274 };
275
276 relayRules = mkOption {
277 description = "Relay rules are used to send certain metrics to a certain backend.";
278 default = null;
279 type = types.uniq (types.nullOr types.string);
280 example = ''
281 [example]
282 pattern = ^mydata\.foo\..+
283 servers = 10.1.2.3, 10.1.2.4:2004, myserver.mydomain.com
284 '';
285 };
286
287 enableAggregator = mkOption {
288 description = "Whether to enable carbon aggregator, the carbon buffering service.";
289 default = false;
290 type = types.bool;
291 };
292
293 aggregationRules = mkOption {
294 description = "Defines if and how received metrics will be aggregated.";
295 default = null;
296 type = types.uniq (types.nullOr types.string);
297 example = ''
298 <env>.applications.<app>.all.requests (60) = sum <env>.applications.<app>.*.requests
299 <env>.applications.<app>.all.latency (60) = avg <env>.applications.<app>.*.latency
300 '';
301 };
302 };
303
304 seyren = {
305 enable = mkOption {
306 description = "Whether to enable seyren service.";
307 default = false;
308 type = types.bool;
309 };
310
311 port = mkOption {
312 description = "Seyren listening port.";
313 default = 8081;
314 type = types.int;
315 };
316
317 seyrenUrl = mkOption {
318 default = "http://localhost:${toString cfg.seyren.port}/";
319 description = "Host where seyren is accessible.";
320 type = types.str;
321 };
322
323 graphiteUrl = mkOption {
324 default = "http://${cfg.web.listenAddress}:${toString cfg.web.port}";
325 description = "Host where graphite service runs.";
326 type = types.str;
327 };
328
329 mongoUrl = mkOption {
330 default = "mongodb://${config.services.mongodb.bind_ip}:27017/seyren";
331 description = "Mongodb connection string.";
332 type = types.str;
333 };
334
335 extraConfig = mkOption {
336 default = {};
337 description = ''
338 Extra seyren configuration. See
339 <link xlink:href='https://github.com/scobal/seyren#config' />
340 '';
341 type = types.attrsOf types.str;
342 example = literalExample ''
343 {
344 GRAPHITE_USERNAME = "user";
345 GRAPHITE_PASSWORD = "pass";
346 }
347 '';
348 };
349 };
350
351 pager = {
352 enable = mkOption {
353 description = ''
354 Whether to enable graphite-pager service. For more information visit
355 <link xlink:href="https://github.com/seatgeek/graphite-pager"/>
356 '';
357 default = false;
358 type = types.bool;
359 };
360
361 redisUrl = mkOption {
362 description = "Redis connection string.";
363 default = "redis://localhost:${toString config.services.redis.port}/";
364 type = types.str;
365 };
366
367 graphiteUrl = mkOption {
368 description = "URL to your graphite service.";
369 default = "http://${cfg.web.listenAddress}:${toString cfg.web.port}";
370 type = types.str;
371 };
372
373 alerts = mkOption {
374 description = "Alerts configuration for graphite-pager.";
375 default = ''
376 alerts:
377 - target: constantLine(100)
378 warning: 90
379 critical: 200
380 name: Test
381 '';
382 example = ''
383 pushbullet_key: pushbullet_api_key
384 alerts:
385 - target: stats.seatgeek.app.deal_quality.venue_info_cache.hit
386 warning: .5
387 critical: 1
388 name: Deal quality venue cache hits
389 '';
390 type = types.lines;
391 };
392 };
393
394 beacon = {
395 enable = mkEnableOption "graphite beacon";
396
397 config = mkOption {
398 description = "Graphite beacon configuration.";
399 default = {};
400 type = types.attrs;
401 };
402 };
403 };
404
405 ###### implementation
406
407 config = mkMerge [
408 (mkIf cfg.carbon.enableCache {
409 systemd.services.carbonCache = let name = "carbon-cache"; in {
410 description = "Graphite Data Storage Backend";
411 wantedBy = [ "multi-user.target" ];
412 after = [ "network.target" ];
413 environment = carbonEnv;
414 serviceConfig = {
415 ExecStart = "${pkgs.pythonPackages.twisted}/bin/twistd ${carbonOpts name}";
416 User = "graphite";
417 Group = "graphite";
418 PermissionsStartOnly = true;
419 PIDFile="/run/${name}/${name}.pid";
420 };
421 preStart = mkPidFileDir name + ''
422
423 mkdir -p ${cfg.dataDir}/whisper
424 chmod 0700 ${cfg.dataDir}/whisper
425 chown graphite:graphite ${cfg.dataDir}
426 chown graphite:graphite ${cfg.dataDir}/whisper
427 '';
428 };
429 })
430
431 (mkIf cfg.carbon.enableAggregator {
432 systemd.services.carbonAggregator = let name = "carbon-aggregator"; in {
433 enable = cfg.carbon.enableAggregator;
434 description = "Carbon Data Aggregator";
435 wantedBy = [ "multi-user.target" ];
436 after = [ "network.target" ];
437 environment = carbonEnv;
438 serviceConfig = {
439 ExecStart = "${pkgs.pythonPackages.twisted}/bin/twistd ${carbonOpts name}";
440 User = "graphite";
441 Group = "graphite";
442 PIDFile="/run/${name}/${name}.pid";
443 };
444 preStart = mkPidFileDir name;
445 };
446 })
447
448 (mkIf cfg.carbon.enableRelay {
449 systemd.services.carbonRelay = let name = "carbon-relay"; in {
450 description = "Carbon Data Relay";
451 wantedBy = [ "multi-user.target" ];
452 after = [ "network.target" ];
453 environment = carbonEnv;
454 serviceConfig = {
455 ExecStart = "${pkgs.pythonPackages.twisted}/bin/twistd ${carbonOpts name}";
456 User = "graphite";
457 Group = "graphite";
458 PIDFile="/run/${name}/${name}.pid";
459 };
460 preStart = mkPidFileDir name;
461 };
462 })
463
464 (mkIf (cfg.carbon.enableCache || cfg.carbon.enableAggregator || cfg.carbon.enableRelay) {
465 environment.systemPackages = [
466 pkgs.pythonPackages.carbon
467 ];
468 })
469
470 (mkIf cfg.web.enable (let
471 python27' = pkgs.python27.override {
472 packageOverrides = self: super: {
473 django = self.django_1_8;
474 django_tagging = self.django_tagging_0_4_3;
475 };
476 };
477 pythonPackages = python27'.pkgs;
478 in {
479 systemd.services.graphiteWeb = {
480 description = "Graphite Web Interface";
481 wantedBy = [ "multi-user.target" ];
482 after = [ "network.target" ];
483 path = [ pkgs.perl ];
484 environment = {
485 PYTHONPATH = let
486 penv = pkgs.python.buildEnv.override {
487 extraLibs = [
488 pythonPackages.graphite_web
489 pythonPackages.pysqlite
490 ];
491 };
492 penvPack = "${penv}/${pkgs.python.sitePackages}";
493 in concatStringsSep ":" [
494 "${graphiteLocalSettingsDir}"
495 "${penvPack}/opt/graphite/webapp"
496 "${penvPack}"
497 # explicitly adding pycairo in path because it cannot be imported via buildEnv
498 "${pkgs.pythonPackages.pycairo}/${pkgs.python.sitePackages}"
499 ];
500 DJANGO_SETTINGS_MODULE = "graphite.settings";
501 GRAPHITE_CONF_DIR = configDir;
502 GRAPHITE_STORAGE_DIR = dataDir;
503 LD_LIBRARY_PATH = "${pkgs.cairo.out}/lib";
504 };
505 serviceConfig = {
506 ExecStart = ''
507 ${pkgs.python27Packages.waitress-django}/bin/waitress-serve-django \
508 --host=${cfg.web.listenAddress} --port=${toString cfg.web.port}
509 '';
510 User = "graphite";
511 Group = "graphite";
512 PermissionsStartOnly = true;
513 };
514 preStart = ''
515 if ! test -e ${dataDir}/db-created; then
516 mkdir -p ${dataDir}/{whisper/,log/webapp/}
517 chmod 0700 ${dataDir}/{whisper/,log/webapp/}
518
519 ${pkgs.pythonPackages.django_1_8}/bin/django-admin.py migrate --noinput
520
521 chown -R graphite:graphite ${dataDir}
522
523 touch ${dataDir}/db-created
524 fi
525
526 # Only collect static files when graphite_web changes.
527 if ! [ "${dataDir}/current_graphite_web" -ef "${pythonPackages.graphite_web}" ]; then
528 mkdir -p ${staticDir}
529 ${pkgs.pythonPackages.django_1_8}/bin/django-admin.py collectstatic --noinput --clear
530 chown -R graphite:graphite ${staticDir}
531 ln -sfT "${pythonPackages.graphite_web}" "${dataDir}/current_graphite_web"
532 fi
533 '';
534 };
535
536 environment.systemPackages = [ pythonPackages.graphite_web ];
537 }))
538
539 (mkIf cfg.api.enable {
540 systemd.services.graphiteApi = {
541 description = "Graphite Api Interface";
542 wantedBy = [ "multi-user.target" ];
543 after = [ "network.target" ];
544 environment = {
545 PYTHONPATH = let
546 aenv = pkgs.python.buildEnv.override {
547 extraLibs = [ cfg.api.package pkgs.cairo pkgs.pythonPackages.cffi ] ++ cfg.api.finders;
548 };
549 in "${aenv}/${pkgs.python.sitePackages}";
550 GRAPHITE_API_CONFIG = graphiteApiConfig;
551 LD_LIBRARY_PATH = "${pkgs.cairo.out}/lib";
552 };
553 serviceConfig = {
554 ExecStart = ''
555 ${pkgs.python27Packages.waitress}/bin/waitress-serve \
556 --host=${cfg.api.listenAddress} --port=${toString cfg.api.port} \
557 graphite_api.app:app
558 '';
559 User = "graphite";
560 Group = "graphite";
561 PermissionsStartOnly = true;
562 };
563 preStart = ''
564 if ! test -e ${dataDir}/db-created; then
565 mkdir -p ${dataDir}/cache/
566 chmod 0700 ${dataDir}/cache/
567
568 chown graphite:graphite ${cfg.dataDir}
569 chown -R graphite:graphite ${cfg.dataDir}/cache
570
571 touch ${dataDir}/db-created
572 fi
573 '';
574 };
575 })
576
577 (mkIf cfg.seyren.enable {
578 systemd.services.seyren = {
579 description = "Graphite Alerting Dashboard";
580 wantedBy = [ "multi-user.target" ];
581 after = [ "network.target" "mongodb.service" ];
582 environment = seyrenConfig;
583 serviceConfig = {
584 ExecStart = "${pkgs.seyren}/bin/seyren -httpPort ${toString cfg.seyren.port}";
585 WorkingDirectory = dataDir;
586 User = "graphite";
587 Group = "graphite";
588 };
589 preStart = ''
590 if ! test -e ${dataDir}/db-created; then
591 mkdir -p ${dataDir}
592 chown graphite:graphite ${dataDir}
593 fi
594 '';
595 };
596
597 services.mongodb.enable = mkDefault true;
598 })
599
600 (mkIf cfg.pager.enable {
601 systemd.services.graphitePager = {
602 description = "Graphite Pager Alerting Daemon";
603 wantedBy = [ "multi-user.target" ];
604 after = [ "network.target" "redis.service" ];
605 environment = {
606 REDIS_URL = cfg.pager.redisUrl;
607 GRAPHITE_URL = cfg.pager.graphiteUrl;
608 };
609 serviceConfig = {
610 ExecStart = "${pkgs.pythonPackages.graphite_pager}/bin/graphite-pager --config ${pagerConfig}";
611 User = "graphite";
612 Group = "graphite";
613 };
614 };
615
616 services.redis.enable = mkDefault true;
617
618 environment.systemPackages = [ pkgs.pythonPackages.graphite_pager ];
619 })
620
621 (mkIf cfg.beacon.enable {
622 systemd.services.graphite-beacon = {
623 description = "Grpahite Beacon Alerting Daemon";
624 wantedBy = [ "multi-user.target" ];
625 serviceConfig = {
626 ExecStart = ''
627 ${pkgs.pythonPackages.graphite_beacon}/bin/graphite-beacon \
628 --config=${pkgs.writeText "graphite-beacon.json" (builtins.toJSON cfg.beacon.config)}
629 '';
630 User = "graphite";
631 Group = "graphite";
632 };
633 };
634 })
635
636 (mkIf (
637 cfg.carbon.enableCache || cfg.carbon.enableAggregator || cfg.carbon.enableRelay ||
638 cfg.web.enable || cfg.api.enable ||
639 cfg.seyren.enable || cfg.pager.enable || cfg.beacon.enable
640 ) {
641 users.extraUsers = singleton {
642 name = "graphite";
643 uid = config.ids.uids.graphite;
644 description = "Graphite daemon user";
645 home = dataDir;
646 };
647 users.extraGroups.graphite.gid = config.ids.gids.graphite;
648 })
649 ];
650}