1 /* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
2
3 #include "stats-common.h"
4 #include "buffer.h"
5 #include "settings-parser.h"
6 #include "service-settings.h"
7 #include "stats-settings.h"
8 #include "array.h"
9
10 /* <settings checks> */
11 #include "event-filter.h"
12 #include <math.h>
13 /* </settings checks> */
14
15 static bool stats_metric_settings_check(void *_set, pool_t pool, const char **error_r);
16 static bool stats_exporter_settings_check(void *_set, pool_t pool, const char **error_r);
17 static bool stats_settings_check(void *_set, pool_t pool, const char **error_r);
18
19 /* <settings checks> */
20 static struct file_listener_settings stats_unix_listeners_array[] = {
21 { "stats-reader", 0600, "", "" },
22 { "stats-writer", 0660, "", "$default_internal_group" },
23 };
24 static struct file_listener_settings *stats_unix_listeners[] = {
25 &stats_unix_listeners_array[0],
26 &stats_unix_listeners_array[1],
27 };
28 static buffer_t stats_unix_listeners_buf = {
29 { { stats_unix_listeners, sizeof(stats_unix_listeners) } }
30 };
31 /* </settings checks> */
32
33 struct service_settings stats_service_settings = {
34 .name = "stats",
35 .protocol = "",
36 .type = "",
37 .executable = "stats",
38 .user = "$default_internal_user",
39 .group = "",
40 .privileged_group = "",
41 .extra_groups = "",
42 .chroot = "",
43
44 .drop_priv_before_exec = FALSE,
45
46 .process_min_avail = 0,
47 .process_limit = 1,
48 .client_limit = 0,
49 .service_count = 0,
50 .idle_kill = UINT_MAX,
51 .vsz_limit = UOFF_T_MAX,
52
53 .unix_listeners = { { &stats_unix_listeners_buf,
54 sizeof(stats_unix_listeners[0]) } },
55 .inet_listeners = ARRAY_INIT,
56 };
57
58 /*
59 * event_exporter { } block settings
60 */
61
62 #undef DEF
63 #define DEF(type, name) \
64 SETTING_DEFINE_STRUCT_##type(#name, name, struct stats_exporter_settings)
65
66 static const struct setting_define stats_exporter_setting_defines[] = {
67 DEF(STR, name),
68 DEF(STR, transport),
69 DEF(STR, transport_args),
70 DEF(TIME_MSECS, transport_timeout),
71 DEF(STR, format),
72 DEF(STR, format_args),
73 SETTING_DEFINE_LIST_END
74 };
75
76 static const struct stats_exporter_settings stats_exporter_default_settings = {
77 .name = "",
78 .transport = "",
79 .transport_args = "",
80 .transport_timeout = 250, /* ms */
81 .format = "",
82 .format_args = "",
83 };
84
85 const struct setting_parser_info stats_exporter_setting_parser_info = {
86 .defines = stats_exporter_setting_defines,
87 .defaults = &stats_exporter_default_settings,
88
89 .type_offset = offsetof(struct stats_exporter_settings, name),
90 .struct_size = sizeof(struct stats_exporter_settings),
91
92 .parent_offset = SIZE_MAX,
93 .check_func = stats_exporter_settings_check,
94 };
95
96 /*
97 * metric { } block settings
98 */
99
100 #undef DEF
101 #define DEF(type, name) \
102 SETTING_DEFINE_STRUCT_##type(#name, name, struct stats_metric_settings)
103
104 static const struct setting_define stats_metric_setting_defines[] = {
105 DEF(STR, metric_name),
106 DEF(STR, fields),
107 DEF(STR, group_by),
108 DEF(STR, filter),
109 DEF(STR, exporter),
110 DEF(STR, exporter_include),
111 DEF(STR, description),
112 SETTING_DEFINE_LIST_END
113 };
114
115 static const struct stats_metric_settings stats_metric_default_settings = {
116 .metric_name = "",
117 .fields = "",
118 .filter = "",
119 .exporter = "",
120 .group_by = "",
121 .exporter_include = STATS_METRIC_SETTINGS_DEFAULT_EXPORTER_INCLUDE,
122 .description = "",
123 };
124
125 const struct setting_parser_info stats_metric_setting_parser_info = {
126 .defines = stats_metric_setting_defines,
127 .defaults = &stats_metric_default_settings,
128
129 .type_offset = offsetof(struct stats_metric_settings, metric_name),
130 .struct_size = sizeof(struct stats_metric_settings),
131
132 .parent_offset = SIZE_MAX,
133 .check_func = stats_metric_settings_check,
134 };
135
136 /*
137 * top-level settings
138 */
139
140 #undef DEF
141 #define DEF(type, name) \
142 SETTING_DEFINE_STRUCT_##type(#name, name, struct stats_settings)
143 #undef DEFLIST_UNIQUE
144 #define DEFLIST_UNIQUE(field, name, defines) \
145 { .type = SET_DEFLIST_UNIQUE, .key = name, \
146 .offset = offsetof(struct stats_settings, field), \
147 .list_info = defines }
148
149 static const struct setting_define stats_setting_defines[] = {
150 DEF(STR, stats_http_rawlog_dir),
151
152 DEFLIST_UNIQUE(metrics, "metric", &stats_metric_setting_parser_info),
153 DEFLIST_UNIQUE(exporters, "event_exporter", &stats_exporter_setting_parser_info),
154 SETTING_DEFINE_LIST_END
155 };
156
157 const struct stats_settings stats_default_settings = {
158 .stats_http_rawlog_dir = "",
159
160 .metrics = ARRAY_INIT,
161 .exporters = ARRAY_INIT,
162 };
163
164 const struct setting_parser_info stats_setting_parser_info = {
165 .module_name = "stats",
166 .defines = stats_setting_defines,
167 .defaults = &stats_default_settings,
168
169 .type_offset = SIZE_MAX,
170 .struct_size = sizeof(struct stats_settings),
171
172 .parent_offset = SIZE_MAX,
173 .check_func = stats_settings_check,
174 };
175
176 /* <settings checks> */
parse_format_args_set_time(struct stats_exporter_settings * set,enum event_exporter_time_fmt fmt,const char ** error_r)177 static bool parse_format_args_set_time(struct stats_exporter_settings *set,
178 enum event_exporter_time_fmt fmt,
179 const char **error_r)
180 {
181 if ((set->parsed_time_format != EVENT_EXPORTER_TIME_FMT_NATIVE) &&
182 (set->parsed_time_format != fmt)) {
183 *error_r = t_strdup_printf("Exporter '%s' specifies multiple "
184 "time format args", set->name);
185 return FALSE;
186 }
187
188 set->parsed_time_format = fmt;
189
190 return TRUE;
191 }
192
parse_format_args(struct stats_exporter_settings * set,const char ** error_r)193 static bool parse_format_args(struct stats_exporter_settings *set,
194 const char **error_r)
195 {
196 const char *const *tmp;
197
198 /* Defaults */
199 set->parsed_time_format = EVENT_EXPORTER_TIME_FMT_NATIVE;
200
201 tmp = t_strsplit_spaces(set->format_args, " ");
202
203 /*
204 * If the config contains multiple types of the same type (e.g.,
205 * both time-rfc3339 and time-unix) we fail the config check.
206 *
207 * Note: At the moment, we have only time-* tokens. In the future
208 * when we have other tokens, they should be parsed here.
209 */
210 for (; *tmp != NULL; tmp++) {
211 enum event_exporter_time_fmt fmt;
212
213 if (strcmp(*tmp, "time-rfc3339") == 0) {
214 fmt = EVENT_EXPORTER_TIME_FMT_RFC3339;
215 } else if (strcmp(*tmp, "time-unix") == 0) {
216 fmt = EVENT_EXPORTER_TIME_FMT_UNIX;
217 } else {
218 *error_r = t_strdup_printf("Unknown exporter format "
219 "arg: %s", *tmp);
220 return FALSE;
221 }
222
223 if (!parse_format_args_set_time(set, fmt, error_r))
224 return FALSE;
225 }
226
227 return TRUE;
228 }
229
stats_exporter_settings_check(void * _set,pool_t pool ATTR_UNUSED,const char ** error_r)230 static bool stats_exporter_settings_check(void *_set, pool_t pool ATTR_UNUSED,
231 const char **error_r)
232 {
233 struct stats_exporter_settings *set = _set;
234 bool time_fmt_required;
235
236 if (set->name[0] == '\0') {
237 *error_r = "Exporter name can't be empty";
238 return FALSE;
239 }
240
241 /* TODO: The following should be plugable.
242 *
243 * Note: Make sure to mirror any changes to the below code in
244 * stats_exporters_add_set().
245 */
246 if (set->format[0] == '\0') {
247 *error_r = "Exporter format name can't be empty";
248 return FALSE;
249 } else if (strcmp(set->format, "none") == 0) {
250 time_fmt_required = FALSE;
251 } else if (strcmp(set->format, "json") == 0) {
252 time_fmt_required = TRUE;
253 } else if (strcmp(set->format, "tab-text") == 0) {
254 time_fmt_required = TRUE;
255 } else {
256 *error_r = t_strdup_printf("Unknown exporter format '%s'",
257 set->format);
258 return FALSE;
259 }
260
261 /* TODO: The following should be plugable.
262 *
263 * Note: Make sure to mirror any changes to the below code in
264 * stats_exporters_add_set().
265 */
266 if (set->transport[0] == '\0') {
267 *error_r = "Exporter transport name can't be empty";
268 return FALSE;
269 } else if (strcmp(set->transport, "drop") == 0 ||
270 strcmp(set->transport, "http-post") == 0 ||
271 strcmp(set->transport, "log") == 0) {
272 /* no-op */
273 } else {
274 *error_r = t_strdup_printf("Unknown transport type '%s'",
275 set->transport);
276 return FALSE;
277 }
278
279 if (!parse_format_args(set, error_r))
280 return FALSE;
281
282 /* Some formats don't have a native way of serializing time stamps */
283 if (time_fmt_required &&
284 set->parsed_time_format == EVENT_EXPORTER_TIME_FMT_NATIVE) {
285 *error_r = t_strdup_printf("%s exporter format requires a "
286 "time-* argument", set->format);
287 return FALSE;
288 }
289
290 return TRUE;
291 }
292
parse_metric_group_by_common(const char * func,const char * const * params,intmax_t * min_r,intmax_t * max_r,intmax_t * other_r,const char ** error_r)293 static bool parse_metric_group_by_common(const char *func,
294 const char *const *params,
295 intmax_t *min_r,
296 intmax_t *max_r,
297 intmax_t *other_r,
298 const char **error_r)
299 {
300 intmax_t min, max, other;
301
302 if ((str_array_length(params) != 3) ||
303 (str_to_intmax(params[0], &min) < 0) ||
304 (str_to_intmax(params[1], &max) < 0) ||
305 (str_to_intmax(params[2], &other) < 0)) {
306 *error_r = t_strdup_printf("group_by '%s' aggregate function takes "
307 "3 int args", func);
308 return FALSE;
309 }
310
311 if ((min < 0) || (max < 0) || (other < 0)) {
312 *error_r = t_strdup_printf("group_by '%s' aggregate function "
313 "arguments must be >= 0", func);
314 return FALSE;
315 }
316
317 if (min >= max) {
318 *error_r = t_strdup_printf("group_by '%s' aggregate function "
319 "min must be < max (%ju must be < %ju)",
320 func, min, max);
321 return FALSE;
322 }
323
324 *min_r = min;
325 *max_r = max;
326 *other_r = other;
327
328 return TRUE;
329 }
330
parse_metric_group_by_exp(pool_t pool,struct stats_metric_settings_group_by * group_by,const char * const * params,const char ** error_r)331 static bool parse_metric_group_by_exp(pool_t pool, struct stats_metric_settings_group_by *group_by,
332 const char *const *params, const char **error_r)
333 {
334 intmax_t min, max, base;
335
336 if (!parse_metric_group_by_common("exponential", params, &min, &max, &base, error_r))
337 return FALSE;
338
339 if ((base != 2) && (base != 10)) {
340 *error_r = t_strdup_printf("group_by 'exponential' aggregate function "
341 "base must be one of: 2, 10 (base=%ju)",
342 base);
343 return FALSE;
344 }
345
346 group_by->func = STATS_METRIC_GROUPBY_QUANTIZED;
347
348 /*
349 * Allocate the bucket range array and fill it in
350 *
351 * The first bucket is special - it contains everything less than or
352 * equal to 'base^min'. The last bucket is also special - it
353 * contains everything greater than 'base^max'.
354 *
355 * The second bucket begins at 'base^min + 1', the third bucket
356 * begins at 'base^(min + 1) + 1', and so on.
357 */
358 group_by->num_ranges = max - min + 2;
359 group_by->ranges = p_new(pool, struct stats_metric_settings_bucket_range,
360 group_by->num_ranges);
361
362 /* set up min & max buckets */
363 group_by->ranges[0].min = INTMAX_MIN;
364 group_by->ranges[0].max = pow(base, min);
365 group_by->ranges[group_by->num_ranges - 1].min = pow(base, max);
366 group_by->ranges[group_by->num_ranges - 1].max = INTMAX_MAX;
367
368 /* remaining buckets */
369 for (unsigned int i = 1; i < group_by->num_ranges - 1; i++) {
370 group_by->ranges[i].min = pow(base, min + (i - 1));
371 group_by->ranges[i].max = pow(base, min + i);
372 }
373
374 return TRUE;
375 }
376
parse_metric_group_by_lin(pool_t pool,struct stats_metric_settings_group_by * group_by,const char * const * params,const char ** error_r)377 static bool parse_metric_group_by_lin(pool_t pool, struct stats_metric_settings_group_by *group_by,
378 const char *const *params, const char **error_r)
379 {
380 intmax_t min, max, step;
381
382 if (!parse_metric_group_by_common("linear", params, &min, &max, &step, error_r))
383 return FALSE;
384
385 if ((min + step) > max) {
386 *error_r = t_strdup_printf("group_by 'linear' aggregate function "
387 "min+step must be <= max (%ju must be <= %ju)",
388 min + step, max);
389 return FALSE;
390 }
391
392 group_by->func = STATS_METRIC_GROUPBY_QUANTIZED;
393
394 /*
395 * Allocate the bucket range array and fill it in
396 *
397 * The first bucket is special - it contains everything less than or
398 * equal to 'min'. The last bucket is also special - it contains
399 * everything greater than 'max'.
400 *
401 * The second bucket begins at 'min + 1', the third bucket begins at
402 * 'min + 1 * step + 1', the fourth at 'min + 2 * step + 1', and so on.
403 */
404 group_by->num_ranges = (max - min) / step + 2;
405 group_by->ranges = p_new(pool, struct stats_metric_settings_bucket_range,
406 group_by->num_ranges);
407
408 /* set up min & max buckets */
409 group_by->ranges[0].min = INTMAX_MIN;
410 group_by->ranges[0].max = min;
411 group_by->ranges[group_by->num_ranges - 1].min = max;
412 group_by->ranges[group_by->num_ranges - 1].max = INTMAX_MAX;
413
414 /* remaining buckets */
415 for (unsigned int i = 1; i < group_by->num_ranges - 1; i++) {
416 group_by->ranges[i].min = min + (i - 1) * step;
417 group_by->ranges[i].max = min + i * step;
418 }
419
420 return TRUE;
421 }
422
parse_metric_group_by(struct stats_metric_settings * set,pool_t pool,const char ** error_r)423 static bool parse_metric_group_by(struct stats_metric_settings *set,
424 pool_t pool, const char **error_r)
425 {
426 const char *const *tmp = t_strsplit_spaces(set->group_by, " ");
427
428 if (tmp[0] == NULL)
429 return TRUE;
430
431 p_array_init(&set->parsed_group_by, pool, str_array_length(tmp));
432
433 /* For each group_by field */
434 for (; *tmp != NULL; tmp++) {
435 struct stats_metric_settings_group_by group_by;
436 const char *const *params;
437
438 i_zero(&group_by);
439
440 /* <field name>:<aggregation func>... */
441 params = t_strsplit(*tmp, ":");
442
443 if (params[1] == NULL) {
444 /* <field name> - alias for <field>:discrete */
445 group_by.func = STATS_METRIC_GROUPBY_DISCRETE;
446 } else if (strcmp(params[1], "discrete") == 0) {
447 /* <field>:discrete */
448 group_by.func = STATS_METRIC_GROUPBY_DISCRETE;
449 if (params[2] != NULL) {
450 *error_r = "group_by 'discrete' aggregate function "
451 "does not take any args";
452 return FALSE;
453 }
454 } else if (strcmp(params[1], "exponential") == 0) {
455 /* <field>:exponential:<min mag>:<max mag>:<base> */
456 if (!parse_metric_group_by_exp(pool, &group_by, ¶ms[2], error_r))
457 return FALSE;
458 } else if (strcmp(params[1], "linear") == 0) {
459 /* <field>:linear:<min val>:<max val>:<step> */
460 if (!parse_metric_group_by_lin(pool, &group_by, ¶ms[2], error_r))
461 return FALSE;
462 } else {
463 *error_r = t_strdup_printf("unknown aggregation function "
464 "'%s' on field '%s'", params[1], params[0]);
465 return FALSE;
466 }
467
468 group_by.field = p_strdup(pool, params[0]);
469
470 array_push_back(&set->parsed_group_by, &group_by);
471 }
472
473 return TRUE;
474 }
475
stats_metric_settings_check(void * _set,pool_t pool,const char ** error_r)476 static bool stats_metric_settings_check(void *_set, pool_t pool, const char **error_r)
477 {
478 struct stats_metric_settings *set = _set;
479
480 if (set->metric_name[0] == '\0') {
481 *error_r = "Metric name can't be empty";
482 return FALSE;
483 }
484
485 if (set->filter[0] == '\0') {
486 *error_r = t_strdup_printf("metric %s { filter } is empty - "
487 "will not match anything", set->metric_name);
488 return FALSE;
489 }
490
491 set->parsed_filter = event_filter_create_fragment(pool);
492 if (event_filter_parse(set->filter, set->parsed_filter, error_r) < 0)
493 return FALSE;
494
495 if (!parse_metric_group_by(set, pool, error_r))
496 return FALSE;
497
498 return TRUE;
499 }
500
stats_settings_check(void * _set,pool_t pool ATTR_UNUSED,const char ** error_r)501 static bool stats_settings_check(void *_set, pool_t pool ATTR_UNUSED,
502 const char **error_r)
503 {
504 struct stats_settings *set = _set;
505 struct stats_exporter_settings *exporter;
506 struct stats_metric_settings *metric;
507
508 if (!array_is_created(&set->metrics) || !array_is_created(&set->exporters))
509 return TRUE;
510
511 /* check that all metrics refer to exporters that exist */
512 array_foreach_elem(&set->metrics, metric) {
513 bool found = FALSE;
514
515 if (metric->exporter[0] == '\0')
516 continue; /* metric not exported */
517
518 array_foreach_elem(&set->exporters, exporter) {
519 if (strcmp(metric->exporter, exporter->name) == 0) {
520 found = TRUE;
521 break;
522 }
523 }
524
525 if (!found) {
526 *error_r = t_strdup_printf("metric %s refers to "
527 "non-existent exporter '%s'",
528 metric->metric_name,
529 metric->exporter);
530 return FALSE;
531 }
532 }
533
534 return TRUE;
535 }
536 /* </settings checks> */
537