summaryrefslogtreecommitdiffstats
path: root/src/stats/stats-settings.c
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/stats/stats-settings.c538
1 files changed, 538 insertions, 0 deletions
diff --git a/src/stats/stats-settings.c b/src/stats/stats-settings.c
new file mode 100644
index 0000000..f75a737
--- /dev/null
+++ b/src/stats/stats-settings.c
@@ -0,0 +1,538 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "stats-common.h"
+#include "buffer.h"
+#include "settings-parser.h"
+#include "service-settings.h"
+#include "stats-settings.h"
+#include "array.h"
+
+/* <settings checks> */
+#include "event-filter.h"
+#include <math.h>
+/* </settings checks> */
+
+static bool stats_metric_settings_check(void *_set, pool_t pool, const char **error_r);
+static bool stats_exporter_settings_check(void *_set, pool_t pool, const char **error_r);
+static bool stats_settings_check(void *_set, pool_t pool, const char **error_r);
+
+/* <settings checks> */
+static struct file_listener_settings stats_unix_listeners_array[] = {
+ { "stats-reader", 0600, "", "" },
+ { "stats-writer", 0660, "", "$default_internal_group" },
+ { "login/stats-writer", 0600, "$default_login_user", "" },
+};
+static struct file_listener_settings *stats_unix_listeners[] = {
+ &stats_unix_listeners_array[0],
+ &stats_unix_listeners_array[1],
+ &stats_unix_listeners_array[2],
+};
+static buffer_t stats_unix_listeners_buf = {
+ { { stats_unix_listeners, sizeof(stats_unix_listeners) } }
+};
+/* </settings checks> */
+
+struct service_settings stats_service_settings = {
+ .name = "stats",
+ .protocol = "",
+ .type = "",
+ .executable = "stats",
+ .user = "$default_internal_user",
+ .group = "",
+ .privileged_group = "",
+ .extra_groups = "",
+ .chroot = "",
+
+ .drop_priv_before_exec = FALSE,
+
+ .process_min_avail = 0,
+ .process_limit = 1,
+ .client_limit = 0,
+ .service_count = 0,
+ .idle_kill = UINT_MAX,
+ .vsz_limit = UOFF_T_MAX,
+
+ .unix_listeners = { { &stats_unix_listeners_buf,
+ sizeof(stats_unix_listeners[0]) } },
+ .inet_listeners = ARRAY_INIT,
+};
+
+/*
+ * event_exporter { } block settings
+ */
+
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct stats_exporter_settings)
+
+static const struct setting_define stats_exporter_setting_defines[] = {
+ DEF(STR, name),
+ DEF(STR, transport),
+ DEF(STR, transport_args),
+ DEF(TIME_MSECS, transport_timeout),
+ DEF(STR, format),
+ DEF(STR, format_args),
+ SETTING_DEFINE_LIST_END
+};
+
+static const struct stats_exporter_settings stats_exporter_default_settings = {
+ .name = "",
+ .transport = "",
+ .transport_args = "",
+ .transport_timeout = 250, /* ms */
+ .format = "",
+ .format_args = "",
+};
+
+const struct setting_parser_info stats_exporter_setting_parser_info = {
+ .defines = stats_exporter_setting_defines,
+ .defaults = &stats_exporter_default_settings,
+
+ .type_offset = offsetof(struct stats_exporter_settings, name),
+ .struct_size = sizeof(struct stats_exporter_settings),
+
+ .parent_offset = SIZE_MAX,
+ .check_func = stats_exporter_settings_check,
+};
+
+/*
+ * metric { } block settings
+ */
+
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct stats_metric_settings)
+
+static const struct setting_define stats_metric_setting_defines[] = {
+ DEF(STR, metric_name),
+ DEF(STR, fields),
+ DEF(STR, group_by),
+ DEF(STR, filter),
+ DEF(STR, exporter),
+ DEF(STR, exporter_include),
+ DEF(STR, description),
+ SETTING_DEFINE_LIST_END
+};
+
+static const struct stats_metric_settings stats_metric_default_settings = {
+ .metric_name = "",
+ .fields = "",
+ .filter = "",
+ .exporter = "",
+ .group_by = "",
+ .exporter_include = STATS_METRIC_SETTINGS_DEFAULT_EXPORTER_INCLUDE,
+ .description = "",
+};
+
+const struct setting_parser_info stats_metric_setting_parser_info = {
+ .defines = stats_metric_setting_defines,
+ .defaults = &stats_metric_default_settings,
+
+ .type_offset = offsetof(struct stats_metric_settings, metric_name),
+ .struct_size = sizeof(struct stats_metric_settings),
+
+ .parent_offset = SIZE_MAX,
+ .check_func = stats_metric_settings_check,
+};
+
+/*
+ * top-level settings
+ */
+
+#undef DEF
+#define DEF(type, name) \
+ SETTING_DEFINE_STRUCT_##type(#name, name, struct stats_settings)
+#undef DEFLIST_UNIQUE
+#define DEFLIST_UNIQUE(field, name, defines) \
+ { .type = SET_DEFLIST_UNIQUE, .key = name, \
+ .offset = offsetof(struct stats_settings, field), \
+ .list_info = defines }
+
+static const struct setting_define stats_setting_defines[] = {
+ DEF(STR, stats_http_rawlog_dir),
+
+ DEFLIST_UNIQUE(metrics, "metric", &stats_metric_setting_parser_info),
+ DEFLIST_UNIQUE(exporters, "event_exporter", &stats_exporter_setting_parser_info),
+ SETTING_DEFINE_LIST_END
+};
+
+const struct stats_settings stats_default_settings = {
+ .stats_http_rawlog_dir = "",
+
+ .metrics = ARRAY_INIT,
+ .exporters = ARRAY_INIT,
+};
+
+const struct setting_parser_info stats_setting_parser_info = {
+ .module_name = "stats",
+ .defines = stats_setting_defines,
+ .defaults = &stats_default_settings,
+
+ .type_offset = SIZE_MAX,
+ .struct_size = sizeof(struct stats_settings),
+
+ .parent_offset = SIZE_MAX,
+ .check_func = stats_settings_check,
+};
+
+/* <settings checks> */
+static bool parse_format_args_set_time(struct stats_exporter_settings *set,
+ enum event_exporter_time_fmt fmt,
+ const char **error_r)
+{
+ if ((set->parsed_time_format != EVENT_EXPORTER_TIME_FMT_NATIVE) &&
+ (set->parsed_time_format != fmt)) {
+ *error_r = t_strdup_printf("Exporter '%s' specifies multiple "
+ "time format args", set->name);
+ return FALSE;
+ }
+
+ set->parsed_time_format = fmt;
+
+ return TRUE;
+}
+
+static bool parse_format_args(struct stats_exporter_settings *set,
+ const char **error_r)
+{
+ const char *const *tmp;
+
+ /* Defaults */
+ set->parsed_time_format = EVENT_EXPORTER_TIME_FMT_NATIVE;
+
+ tmp = t_strsplit_spaces(set->format_args, " ");
+
+ /*
+ * If the config contains multiple types of the same type (e.g.,
+ * both time-rfc3339 and time-unix) we fail the config check.
+ *
+ * Note: At the moment, we have only time-* tokens. In the future
+ * when we have other tokens, they should be parsed here.
+ */
+ for (; *tmp != NULL; tmp++) {
+ enum event_exporter_time_fmt fmt;
+
+ if (strcmp(*tmp, "time-rfc3339") == 0) {
+ fmt = EVENT_EXPORTER_TIME_FMT_RFC3339;
+ } else if (strcmp(*tmp, "time-unix") == 0) {
+ fmt = EVENT_EXPORTER_TIME_FMT_UNIX;
+ } else {
+ *error_r = t_strdup_printf("Unknown exporter format "
+ "arg: %s", *tmp);
+ return FALSE;
+ }
+
+ if (!parse_format_args_set_time(set, fmt, error_r))
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static bool stats_exporter_settings_check(void *_set, pool_t pool ATTR_UNUSED,
+ const char **error_r)
+{
+ struct stats_exporter_settings *set = _set;
+ bool time_fmt_required;
+
+ if (set->name[0] == '\0') {
+ *error_r = "Exporter name can't be empty";
+ return FALSE;
+ }
+
+ /* TODO: The following should be plugable.
+ *
+ * Note: Make sure to mirror any changes to the below code in
+ * stats_exporters_add_set().
+ */
+ if (set->format[0] == '\0') {
+ *error_r = "Exporter format name can't be empty";
+ return FALSE;
+ } else if (strcmp(set->format, "none") == 0) {
+ time_fmt_required = FALSE;
+ } else if (strcmp(set->format, "json") == 0) {
+ time_fmt_required = TRUE;
+ } else if (strcmp(set->format, "tab-text") == 0) {
+ time_fmt_required = TRUE;
+ } else {
+ *error_r = t_strdup_printf("Unknown exporter format '%s'",
+ set->format);
+ return FALSE;
+ }
+
+ /* TODO: The following should be plugable.
+ *
+ * Note: Make sure to mirror any changes to the below code in
+ * stats_exporters_add_set().
+ */
+ if (set->transport[0] == '\0') {
+ *error_r = "Exporter transport name can't be empty";
+ return FALSE;
+ } else if (strcmp(set->transport, "drop") == 0 ||
+ strcmp(set->transport, "http-post") == 0 ||
+ strcmp(set->transport, "log") == 0) {
+ /* no-op */
+ } else {
+ *error_r = t_strdup_printf("Unknown transport type '%s'",
+ set->transport);
+ return FALSE;
+ }
+
+ if (!parse_format_args(set, error_r))
+ return FALSE;
+
+ /* Some formats don't have a native way of serializing time stamps */
+ if (time_fmt_required &&
+ set->parsed_time_format == EVENT_EXPORTER_TIME_FMT_NATIVE) {
+ *error_r = t_strdup_printf("%s exporter format requires a "
+ "time-* argument", set->format);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static bool 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)
+{
+ intmax_t min, max, other;
+
+ if ((str_array_length(params) != 3) ||
+ (str_to_intmax(params[0], &min) < 0) ||
+ (str_to_intmax(params[1], &max) < 0) ||
+ (str_to_intmax(params[2], &other) < 0)) {
+ *error_r = t_strdup_printf("group_by '%s' aggregate function takes "
+ "3 int args", func);
+ return FALSE;
+ }
+
+ if ((min < 0) || (max < 0) || (other < 0)) {
+ *error_r = t_strdup_printf("group_by '%s' aggregate function "
+ "arguments must be >= 0", func);
+ return FALSE;
+ }
+
+ if (min >= max) {
+ *error_r = t_strdup_printf("group_by '%s' aggregate function "
+ "min must be < max (%ju must be < %ju)",
+ func, min, max);
+ return FALSE;
+ }
+
+ *min_r = min;
+ *max_r = max;
+ *other_r = other;
+
+ return TRUE;
+}
+
+static bool parse_metric_group_by_exp(pool_t pool, struct stats_metric_settings_group_by *group_by,
+ const char *const *params, const char **error_r)
+{
+ intmax_t min, max, base;
+
+ if (!parse_metric_group_by_common("exponential", params, &min, &max, &base, error_r))
+ return FALSE;
+
+ if ((base != 2) && (base != 10)) {
+ *error_r = t_strdup_printf("group_by 'exponential' aggregate function "
+ "base must be one of: 2, 10 (base=%ju)",
+ base);
+ return FALSE;
+ }
+
+ group_by->func = STATS_METRIC_GROUPBY_QUANTIZED;
+
+ /*
+ * Allocate the bucket range array and fill it in
+ *
+ * The first bucket is special - it contains everything less than or
+ * equal to 'base^min'. The last bucket is also special - it
+ * contains everything greater than 'base^max'.
+ *
+ * The second bucket begins at 'base^min + 1', the third bucket
+ * begins at 'base^(min + 1) + 1', and so on.
+ */
+ group_by->num_ranges = max - min + 2;
+ group_by->ranges = p_new(pool, struct stats_metric_settings_bucket_range,
+ group_by->num_ranges);
+
+ /* set up min & max buckets */
+ group_by->ranges[0].min = INTMAX_MIN;
+ group_by->ranges[0].max = pow(base, min);
+ group_by->ranges[group_by->num_ranges - 1].min = pow(base, max);
+ group_by->ranges[group_by->num_ranges - 1].max = INTMAX_MAX;
+
+ /* remaining buckets */
+ for (unsigned int i = 1; i < group_by->num_ranges - 1; i++) {
+ group_by->ranges[i].min = pow(base, min + (i - 1));
+ group_by->ranges[i].max = pow(base, min + i);
+ }
+
+ return TRUE;
+}
+
+static bool parse_metric_group_by_lin(pool_t pool, struct stats_metric_settings_group_by *group_by,
+ const char *const *params, const char **error_r)
+{
+ intmax_t min, max, step;
+
+ if (!parse_metric_group_by_common("linear", params, &min, &max, &step, error_r))
+ return FALSE;
+
+ if ((min + step) > max) {
+ *error_r = t_strdup_printf("group_by 'linear' aggregate function "
+ "min+step must be <= max (%ju must be <= %ju)",
+ min + step, max);
+ return FALSE;
+ }
+
+ group_by->func = STATS_METRIC_GROUPBY_QUANTIZED;
+
+ /*
+ * Allocate the bucket range array and fill it in
+ *
+ * The first bucket is special - it contains everything less than or
+ * equal to 'min'. The last bucket is also special - it contains
+ * everything greater than 'max'.
+ *
+ * The second bucket begins at 'min + 1', the third bucket begins at
+ * 'min + 1 * step + 1', the fourth at 'min + 2 * step + 1', and so on.
+ */
+ group_by->num_ranges = (max - min) / step + 2;
+ group_by->ranges = p_new(pool, struct stats_metric_settings_bucket_range,
+ group_by->num_ranges);
+
+ /* set up min & max buckets */
+ group_by->ranges[0].min = INTMAX_MIN;
+ group_by->ranges[0].max = min;
+ group_by->ranges[group_by->num_ranges - 1].min = max;
+ group_by->ranges[group_by->num_ranges - 1].max = INTMAX_MAX;
+
+ /* remaining buckets */
+ for (unsigned int i = 1; i < group_by->num_ranges - 1; i++) {
+ group_by->ranges[i].min = min + (i - 1) * step;
+ group_by->ranges[i].max = min + i * step;
+ }
+
+ return TRUE;
+}
+
+static bool parse_metric_group_by(struct stats_metric_settings *set,
+ pool_t pool, const char **error_r)
+{
+ const char *const *tmp = t_strsplit_spaces(set->group_by, " ");
+
+ if (tmp[0] == NULL)
+ return TRUE;
+
+ p_array_init(&set->parsed_group_by, pool, str_array_length(tmp));
+
+ /* For each group_by field */
+ for (; *tmp != NULL; tmp++) {
+ struct stats_metric_settings_group_by group_by;
+ const char *const *params;
+
+ i_zero(&group_by);
+
+ /* <field name>:<aggregation func>... */
+ params = t_strsplit(*tmp, ":");
+
+ if (params[1] == NULL) {
+ /* <field name> - alias for <field>:discrete */
+ group_by.func = STATS_METRIC_GROUPBY_DISCRETE;
+ } else if (strcmp(params[1], "discrete") == 0) {
+ /* <field>:discrete */
+ group_by.func = STATS_METRIC_GROUPBY_DISCRETE;
+ if (params[2] != NULL) {
+ *error_r = "group_by 'discrete' aggregate function "
+ "does not take any args";
+ return FALSE;
+ }
+ } else if (strcmp(params[1], "exponential") == 0) {
+ /* <field>:exponential:<min mag>:<max mag>:<base> */
+ if (!parse_metric_group_by_exp(pool, &group_by, &params[2], error_r))
+ return FALSE;
+ } else if (strcmp(params[1], "linear") == 0) {
+ /* <field>:linear:<min val>:<max val>:<step> */
+ if (!parse_metric_group_by_lin(pool, &group_by, &params[2], error_r))
+ return FALSE;
+ } else {
+ *error_r = t_strdup_printf("unknown aggregation function "
+ "'%s' on field '%s'", params[1], params[0]);
+ return FALSE;
+ }
+
+ group_by.field = p_strdup(pool, params[0]);
+
+ array_push_back(&set->parsed_group_by, &group_by);
+ }
+
+ return TRUE;
+}
+
+static bool stats_metric_settings_check(void *_set, pool_t pool, const char **error_r)
+{
+ struct stats_metric_settings *set = _set;
+
+ if (set->metric_name[0] == '\0') {
+ *error_r = "Metric name can't be empty";
+ return FALSE;
+ }
+
+ if (set->filter[0] == '\0') {
+ *error_r = t_strdup_printf("metric %s { filter } is empty - "
+ "will not match anything", set->metric_name);
+ return FALSE;
+ }
+
+ set->parsed_filter = event_filter_create_fragment(pool);
+ if (event_filter_parse(set->filter, set->parsed_filter, error_r) < 0)
+ return FALSE;
+
+ if (!parse_metric_group_by(set, pool, error_r))
+ return FALSE;
+
+ return TRUE;
+}
+
+static bool stats_settings_check(void *_set, pool_t pool ATTR_UNUSED,
+ const char **error_r)
+{
+ struct stats_settings *set = _set;
+ struct stats_exporter_settings *exporter;
+ struct stats_metric_settings *metric;
+
+ if (!array_is_created(&set->metrics) || !array_is_created(&set->exporters))
+ return TRUE;
+
+ /* check that all metrics refer to exporters that exist */
+ array_foreach_elem(&set->metrics, metric) {
+ bool found = FALSE;
+
+ if (metric->exporter[0] == '\0')
+ continue; /* metric not exported */
+
+ array_foreach_elem(&set->exporters, exporter) {
+ if (strcmp(metric->exporter, exporter->name) == 0) {
+ found = TRUE;
+ break;
+ }
+ }
+
+ if (!found) {
+ *error_r = t_strdup_printf("metric %s refers to "
+ "non-existent exporter '%s'",
+ metric->metric_name,
+ metric->exporter);
+ return FALSE;
+ }
+ }
+
+ return TRUE;
+}
+/* </settings checks> */