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, &params[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, &params[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