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