]> git.ipfire.org Git - thirdparty/dovecot/core.git/commitdiff
stats: Expand metric_group_by setting to a hierarchy of settings
authorTimo Sirainen <timo.sirainen@open-xchange.com>
Mon, 2 Sep 2024 10:18:37 +0000 (13:18 +0300)
committerAki Tuomi <aki.tuomi@open-xchange.com>
Fri, 17 Jan 2025 08:39:59 +0000 (10:39 +0200)
doveadm stats add --group-by preserves the old syntax for now.

src/stats/client-reader.c
src/stats/stats-metrics.c
src/stats/stats-metrics.h
src/stats/stats-settings.c
src/stats/stats-settings.h
src/stats/test-client-reader.c
src/stats/test-stats-metrics.c

index e9fabe44bb68802a2106a3582902ad2a3088dd30..d313e067a077062ec6ff07f7cadbe2fc36edd57c 100644 (file)
@@ -159,6 +159,7 @@ static int
 reader_client_input_metrics_add(struct reader_client *client,
                                const char *const *args)
 {
+       ARRAY_TYPE(stats_metric_settings_group_by) group_by;
        const char *error;
 
        if (str_array_length(args) < 7) {
@@ -173,10 +174,16 @@ reader_client_input_metrics_add(struct reader_client *client,
        set->pool = pool;
        set->name = p_strdup(pool, args[0]);
        set->description = p_strdup(pool, args[1]);
-       set->group_by = p_strdup(pool, args[3]);
        set->filter = p_strdup(pool, args[4]);
        set->exporter = p_strdup(pool, args[5]);
 
+       if (!parse_legacy_metric_group_by(pool, args[3], &group_by, &error)) {
+               e_error(client->conn.event,
+                       "METRICS-ADD: Invalid metric_group_by: %s", error);
+               pool_unref(&pool);
+               return -1;
+       }
+
        p_array_init(&set->fields, pool, 4);
        if (settings_parse_boollist_string(args[2], pool, &set->fields,
                                           &error) < 0) {
@@ -204,7 +211,7 @@ reader_client_input_metrics_add(struct reader_client *client,
        }
 
        o_stream_cork(client->conn.output);
-       if (stats_metrics_add_dynamic(stats_metrics, set, &error)) {
+       if (stats_metrics_add_dynamic(stats_metrics, set, &group_by, &error)) {
                client_writer_update_connections();
                o_stream_nsend(client->conn.output, "+", 1);
        } else {
index b7b8491aacb28b2b0e7d01c082e2178e7880a2f0..048cd9c9532eda959f8815a5c09b4bd3c29591b1 100644 (file)
@@ -88,9 +88,7 @@ static int stats_exporters_add_filter(struct stats_metrics *metrics,
                ret = -1;
        } else {
                struct event *event = event_create(metrics->event);
-               event_set_ptr(event, SETTINGS_EVENT_FILTER_NAME,
-                       p_strdup_printf(event_get_pool(event),
-                                       "event_exporter/%s", filter_name));
+               event_add_str(event, "event_exporter", filter_name);
                ret = stats_exporters_add_set(metrics, event, set, error_r);
                event_unref(&event);
        }
@@ -135,6 +133,7 @@ stats_metrics_exporter_find(struct stats_metrics *metrics,
 
 static int stats_metrics_add_set(struct stats_metrics *metrics,
                                 const struct stats_metric_settings *set,
+                                ARRAY_TYPE(stats_metric_settings_group_by) *group_by,
                                 const char **error_r)
 {
        struct event_exporter *exporter = NULL;
@@ -156,9 +155,8 @@ static int stats_metrics_add_set(struct stats_metrics *metrics,
        fields = settings_boollist_get(&set->fields);
        metric = stats_metric_alloc(metrics->pool, set->name, set, fields);
 
-       if (array_is_created(&set->parsed_group_by))
-               metric->group_by = array_get(&set->parsed_group_by,
-                                            &metric->group_by_count);
+       if (array_is_created(group_by))
+               metric->group_by = array_get(group_by, &metric->group_by_count);
 
        array_push_back(&metrics->metrics, &metric);
 
@@ -195,11 +193,137 @@ static int stats_metrics_add_set(struct stats_metrics *metrics,
        return 0;
 }
 
+static bool
+stats_metrics_group_by_exponential_check(const struct stats_metric_group_by_method_settings *set,
+                                        const char **error_r)
+{
+       if (set->exponential_base != 2 && set->exponential_base != 10) {
+               *error_r = "metric_group_by_method_exponential_base must be 2 or 10";
+               return FALSE;
+       }
+       return TRUE;
+}
+
+static bool
+stats_metrics_group_by_linear_check(const struct stats_metric_group_by_method_settings *set,
+                                   const char **error_r)
+{
+       if (set->linear_step == 0) {
+               *error_r = "metric_group_by_method_linear_step must not be 0";
+               return FALSE;
+       }
+       if (set->linear_min >= set->linear_max) {
+               *error_r = t_strdup_printf(
+                       "metric_group_by_method_linear_min (%ju) must be smaller than "
+                       "metric_group_by_method_linear_max (%ju)",
+                       set->linear_min, set->linear_max);
+               return FALSE;
+       }
+       if ((set->linear_min + set->linear_step) > set->linear_max) {
+               *error_r = t_strdup_printf(
+                       "metric_group_by_method_linear_min (%ju) + "
+                       "metric_group_by_method_linear_step (%ju) must be <= "
+                       "metric_group_by_method_linear_max (%ju)",
+                       set->linear_min, set->linear_step, set->linear_max);
+               return FALSE;
+       }
+       return TRUE;
+}
+
+static int
+stats_metrics_get_group_by_method(struct event *event, pool_t pool,
+                                 struct stats_metric_settings_group_by *group_by,
+                                 const char **error_r)
+{
+       const struct stats_metric_group_by_method_settings *set;
+
+       if (settings_get(event, &stats_metric_group_by_method_setting_parser_info,
+                        0, &set, error_r) < 0)
+               return -1;
+
+       if (strcmp(set->method, "discrete") == 0) {
+               group_by->func = STATS_METRIC_GROUPBY_DISCRETE;
+               group_by->discrete_modifier =
+                       p_strdup_empty(pool, set->discrete_modifier);
+       } else if (strcmp(set->method, "exponential") == 0) {
+               if (!stats_metrics_group_by_exponential_check(set, error_r))
+                       return -1;
+               metrics_group_by_exponential_init(group_by, pool,
+                       set->exponential_base,
+                       set->exponential_min_magnitude,
+                       set->exponential_max_magnitude);
+       } else if (strcmp(set->method, "linear") == 0) {
+               if (!stats_metrics_group_by_linear_check(set, error_r))
+                       return -1;
+               metrics_group_by_linear_init(group_by, pool,
+                       set->linear_min, set->linear_max, set->linear_step);
+       } else {
+               i_unreached();
+       }
+
+       settings_free(set);
+       return 0;
+}
+
+static int
+stats_metrics_get_group_by(struct event *event,
+                          const struct stats_metric_settings *set,
+                          ARRAY_TYPE(stats_metric_settings_group_by) *group_by_r,
+                          const char **error_r)
+{
+       const struct stats_metric_group_by_settings *group_by_set;
+       const char *group_by_name;
+
+       if (array_is_empty(&set->group_by)) {
+               i_zero(group_by_r);
+               return 0;
+       }
+       p_array_init(group_by_r, set->pool, array_count(&set->group_by));
+       array_foreach_elem(&set->group_by, group_by_name) {
+               if (settings_get_filter(event,
+                               "metric_group_by", group_by_name,
+                               &stats_metric_group_by_setting_parser_info,
+                               0, &group_by_set, error_r) < 0)
+                       return -1;
+
+               struct stats_metric_settings_group_by *group_by =
+                       array_append_space(group_by_r);
+               group_by->field = p_strdup(set->pool, group_by_set->field);
+
+               int ret = 0;
+               if (array_is_empty(&group_by_set->method)) {
+                       /* default to discrete */
+                       group_by->func = STATS_METRIC_GROUPBY_DISCRETE;
+               } else if (array_count(&group_by_set->method) > 1) {
+                       *error_r = "Only one metric_group_by_method named filter is allowed";
+                       ret = -1;
+               } else {
+                       struct event *group_event = event_create(event);
+                       event_add_str(group_event, "metric_group_by",
+                                     group_by_name);
+                       struct event *method_event = event_create(group_event);
+                       event_add_str(group_event, "metric_group_by_method",
+                                     array_idx_elem(&group_by_set->method, 0));
+                       ret = stats_metrics_get_group_by_method(method_event, set->pool,
+                                                               group_by, error_r);
+                       event_unref(&method_event);
+                       event_unref(&group_event);
+               }
+               settings_free(group_by_set);
+               if (ret < 0) {
+                       *error_r = t_strdup_printf("metric_group_by %s: %s",
+                                                  group_by_name, *error_r);
+                       return -1;
+               }
+       }
+       return 0;
+}
+
 static int stats_metrics_add_filter(struct stats_metrics *metrics,
                                    const char *filter_name,
                                    const char **error_r)
 {
-       struct stats_metric_settings *set;
+       const struct stats_metric_settings *set;
        int ret = 0;
 
        if (settings_get_filter(metrics->event, "metric", filter_name,
@@ -211,7 +335,13 @@ static int stats_metrics_add_filter(struct stats_metrics *metrics,
                *error_r = "Metric name can't be empty";
                ret = -1;
        } else {
-               ret = stats_metrics_add_set(metrics, set, error_r);
+               ARRAY_TYPE(stats_metric_settings_group_by) group_by;
+               struct event *event = event_create(metrics->event);
+               event_add_str(event, "metric", filter_name);
+               ret = stats_metrics_get_group_by(event, set, &group_by, error_r);
+               if (ret == 0)
+                       ret = stats_metrics_add_set(metrics, set, &group_by, error_r);
+               event_unref(&event);
        }
        settings_free(set);
        return ret;
@@ -256,6 +386,7 @@ stats_metrics_check_for_exporter(struct stats_metrics *metrics, const char *name
 
 bool stats_metrics_add_dynamic(struct stats_metrics *metrics,
                               const struct stats_metric_settings *set,
+                              ARRAY_TYPE(stats_metric_settings_group_by) *group_by,
                               const char **error_r)
 {
        unsigned int existing_idx ATTR_UNUSED;
@@ -270,7 +401,7 @@ bool stats_metrics_add_dynamic(struct stats_metrics *metrics,
                return FALSE;
        }
 
-       if (stats_metrics_add_set(metrics, set, error_r) < 0)
+       if (stats_metrics_add_set(metrics, set, group_by, error_r) < 0)
                return FALSE;
        return TRUE;
 }
index 18915e1d6e0471b753cad12a46ea24d69515b789..e9b08741cd2c283c4f22ea7823de5f3802ad85e4 100644 (file)
@@ -100,6 +100,7 @@ struct metric {
 
 bool stats_metrics_add_dynamic(struct stats_metrics *metrics,
                               const struct stats_metric_settings *set,
+                              ARRAY_TYPE(stats_metric_settings_group_by) *group_by,
                               const char **error_r);
 
 bool stats_metrics_remove_dynamic(struct stats_metrics *metrics,
index fb67b4858f99fcc7a82e3540a2272621bb7bf666..43a6ecfc9c85d242fe910f4a9c988c8c523e49ca 100644 (file)
@@ -93,6 +93,81 @@ const struct setting_parser_info stats_exporter_setting_parser_info = {
        .check_func = stats_exporter_settings_check,
 };
 
+/*
+ * metric_group_by { } block settings
+ */
+
+#undef DEF
+#define DEF(type, name) \
+       SETTING_DEFINE_STRUCT_##type("metric_group_by_"#name, name, struct stats_metric_group_by_settings)
+
+static const struct setting_define stats_metric_group_by_setting_defines[] = {
+       DEF(STR, field),
+
+       { .type = SET_FILTER_ARRAY, .key = "metric_group_by_method",
+         .offset = offsetof(struct stats_metric_group_by_settings, method),
+         .filter_array_field_name = "metric_group_by_method_method", },
+
+       SETTING_DEFINE_LIST_END
+};
+
+static const struct stats_metric_group_by_settings stats_metric_group_by_default_settings = {
+       .field = "",
+       .method = ARRAY_INIT,
+};
+
+const struct setting_parser_info stats_metric_group_by_setting_parser_info = {
+       .name = "stats_metric_group_by",
+
+       .defines = stats_metric_group_by_setting_defines,
+       .defaults = &stats_metric_group_by_default_settings,
+
+       .struct_size = sizeof(struct stats_metric_group_by_settings),
+       .pool_offset1 = 1 + offsetof(struct stats_metric_group_by_settings, pool),
+};
+
+/*
+ * metric_group_by_method { } block settings
+ */
+
+#undef DEF
+#define DEF(type, name) \
+       SETTING_DEFINE_STRUCT_##type("metric_group_by_method_"#name, name, struct stats_metric_group_by_method_settings)
+
+static const struct setting_define stats_metric_group_by_method_setting_defines[] = {
+       DEF(ENUM, method),
+       DEF(STR_NOVARS, discrete_modifier),
+       DEF(UINT, exponential_min_magnitude),
+       DEF(UINT, exponential_max_magnitude),
+       DEF(UINT, exponential_base),
+       DEF(UINTMAX, linear_min),
+       DEF(UINTMAX, linear_max),
+       DEF(UINTMAX, linear_step),
+
+       SETTING_DEFINE_LIST_END
+};
+
+static const struct stats_metric_group_by_method_settings stats_metric_group_by_method_default_settings = {
+       .method = "discrete:exponential:linear",
+       .discrete_modifier = "",
+       .exponential_min_magnitude = 0,
+       .exponential_max_magnitude = 0,
+       .exponential_base = 10,
+       .linear_min = 0,
+       .linear_max = 0,
+       .linear_step = 0,
+};
+
+const struct setting_parser_info stats_metric_group_by_method_setting_parser_info = {
+       .name = "stats_metric_group_by_",
+
+       .defines = stats_metric_group_by_method_setting_defines,
+       .defaults = &stats_metric_group_by_method_default_settings,
+
+       .struct_size = sizeof(struct stats_metric_group_by_method_settings),
+       .pool_offset1 = 1 + offsetof(struct stats_metric_group_by_method_settings, pool),
+};
+
 /*
  * metric { } block settings
  */
@@ -104,11 +179,15 @@ const struct setting_parser_info stats_exporter_setting_parser_info = {
 static const struct setting_define stats_metric_setting_defines[] = {
        DEF(STR, name),
        DEF(BOOLLIST, fields),
-       DEF(STR_NOVARS, group_by),
        DEF(STR, filter),
        DEF(STR, exporter),
        DEF(BOOLLIST, exporter_include),
        DEF(STR, description),
+
+       { .type = SET_FILTER_ARRAY, .key = "metric_group_by",
+         .offset = offsetof(struct stats_metric_settings, group_by),
+         .filter_array_field_name = "metric_group_by_field", },
+
        SETTING_DEFINE_LIST_END
 };
 
@@ -117,7 +196,7 @@ const struct stats_metric_settings stats_metric_default_settings = {
        .fields = ARRAY_INIT,
        .filter = "",
        .exporter = "",
-       .group_by = "",
+       .group_by = ARRAY_INIT,
        .description = "",
 };
 
@@ -222,10 +301,18 @@ static bool stats_exporter_settings_check(void *_set, pool_t pool ATTR_UNUSED,
        return TRUE;
 }
 
-static void
-metrics_group_by_exponential_init(struct stats_metric_settings_group_by *group_by,
-                                 pool_t pool, unsigned int base,
-                                 unsigned int min, unsigned int max)
+#ifdef CONFIG_BINARY
+void metrics_group_by_exponential_init(struct stats_metric_settings_group_by *group_by,
+                                      pool_t pool, unsigned int base,
+                                      unsigned int min, unsigned int max);
+void metrics_group_by_linear_init(struct stats_metric_settings_group_by *group_by,
+                                 pool_t pool, uint64_t min, uint64_t max,
+                                 uint64_t step);
+#endif
+
+void metrics_group_by_exponential_init(struct stats_metric_settings_group_by *group_by,
+                                      pool_t pool, unsigned int base,
+                                      unsigned int min, unsigned int max)
 {
        group_by->func = STATS_METRIC_GROUPBY_QUANTIZED;
        /*
@@ -255,10 +342,9 @@ metrics_group_by_exponential_init(struct stats_metric_settings_group_by *group_b
        }
 }
 
-static void
-metrics_group_by_linear_init(struct stats_metric_settings_group_by *group_by,
-                            pool_t pool, uint64_t min, uint64_t max,
-                            uint64_t step)
+void metrics_group_by_linear_init(struct stats_metric_settings_group_by *group_by,
+                                 pool_t pool, uint64_t min, uint64_t max,
+                                 uint64_t step)
 {
        group_by->func = STATS_METRIC_GROUPBY_QUANTIZED;
        /*
@@ -271,6 +357,7 @@ metrics_group_by_linear_init(struct stats_metric_settings_group_by *group_by,
         * 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.
         */
+       i_assert(step > 0);
        group_by->num_ranges = (max - min) / step + 2;
        group_by->ranges = p_new(pool, struct stats_metric_settings_bucket_range,
                                 group_by->num_ranges);
@@ -287,6 +374,7 @@ metrics_group_by_linear_init(struct stats_metric_settings_group_by *group_by,
                group_by->ranges[i].max = min + i * step;
        }
 }
+/* </settings checks> */
 
 static bool parse_metric_group_by_common(const char *func,
                                         const char *const *params,
@@ -395,15 +483,17 @@ parse_metric_group_by_mod(pool_t pool,
        return TRUE;
 }
 
-static bool parse_metric_group_by(struct stats_metric_settings *set,
-                                 pool_t pool, const char **error_r)
+bool parse_legacy_metric_group_by(pool_t pool, const char *group_by_str,
+                                 ARRAY_TYPE(stats_metric_settings_group_by) *group_by_r,
+                                 const char **error_r)
 {
-       const char *const *tmp = t_strsplit_spaces(set->group_by, " ");
+       const char *const *tmp = t_strsplit_spaces(group_by_str, " ");
 
+       i_zero(group_by_r);
        if (tmp[0] == NULL)
                return TRUE;
 
-       p_array_init(&set->parsed_group_by, pool, str_array_length(tmp));
+       p_array_init(group_by_r, pool, str_array_length(tmp));
 
        /* For each group_by field */
        for (; *tmp != NULL; tmp++) {
@@ -438,12 +528,13 @@ static bool parse_metric_group_by(struct stats_metric_settings *set,
                        return FALSE;
                }
 
-               array_push_back(&set->parsed_group_by, &group_by);
+               array_push_back(group_by_r, &group_by);
        }
 
        return TRUE;
 }
 
+/* <settings checks> */
 static bool stats_metric_settings_check(void *_set, pool_t pool, const char **error_r)
 {
        struct stats_metric_settings *set = _set;
@@ -461,9 +552,6 @@ static bool stats_metric_settings_check(void *_set, pool_t pool, const char **er
        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;
 }
 
index a78fe2401da92a04031d4ccdbfc806ac3d23755e..a89a1e1ab3380c40281faa457490a27c97718ef4 100644 (file)
@@ -74,6 +74,28 @@ struct stats_exporter_settings {
        enum event_exporter_time_fmt parsed_time_format;
 };
 
+struct stats_metric_group_by_settings {
+       pool_t pool;
+       const char *field;
+       ARRAY_TYPE(const_string) method;
+};
+
+struct stats_metric_group_by_method_settings {
+       pool_t pool;
+
+       const char *method;
+
+       const char *discrete_modifier;
+
+       unsigned int exponential_min_magnitude;
+       unsigned int exponential_max_magnitude;
+       unsigned int exponential_base;
+
+       uintmax_t linear_min;
+       uintmax_t linear_max;
+       uintmax_t linear_step;
+};
+
 /* <settings checks> */
 enum stats_metric_group_by_func {
        STATS_METRIC_GROUPBY_DISCRETE = 0,
@@ -98,6 +120,8 @@ struct stats_metric_settings_group_by {
        unsigned int num_ranges;
        struct stats_metric_settings_bucket_range *ranges;
 };
+ARRAY_DEFINE_TYPE(stats_metric_settings_group_by,
+                 struct stats_metric_settings_group_by);
 /* </settings checks> */
 
 struct stats_metric_settings {
@@ -106,10 +130,9 @@ struct stats_metric_settings {
        const char *name;
        const char *description;
        ARRAY_TYPE(const_string) fields;
-       const char *group_by;
+       ARRAY_TYPE(const_string) group_by;
        const char *filter;
 
-       ARRAY(struct stats_metric_settings_group_by) parsed_group_by;
        struct event_filter *parsed_filter;
 
        /* exporter related fields */
@@ -126,8 +149,20 @@ struct stats_settings {
 
 extern const struct setting_parser_info stats_setting_parser_info;
 extern const struct setting_parser_info stats_metric_setting_parser_info;
+extern const struct setting_parser_info stats_metric_group_by_setting_parser_info;
+extern const struct setting_parser_info stats_metric_group_by_method_setting_parser_info;
 extern const struct setting_parser_info stats_exporter_setting_parser_info;
 
 extern const struct stats_metric_settings stats_metric_default_settings;
 
+bool parse_legacy_metric_group_by(pool_t pool, const char *group_by_str,
+                                 ARRAY_TYPE(stats_metric_settings_group_by) *group_by_r,
+                                 const char **error_r);
+void metrics_group_by_exponential_init(struct stats_metric_settings_group_by *group_by,
+                                      pool_t pool, unsigned int base,
+                                      unsigned int min, unsigned int max);
+void metrics_group_by_linear_init(struct stats_metric_settings_group_by *group_by,
+                                 pool_t pool, uint64_t min, uint64_t max,
+                                 uint64_t step);
+
 #endif
index 9678ce6fea8b534a545c3f433792a4a807815b08..8627e8230465c80c2e841712f31dce4ba2e7f667 100644 (file)
@@ -128,7 +128,12 @@ static const char *const settings_blob_2[] = {
        "metric=test",
        "metric/test/metric_name=test",
        "metric/test/filter=event=test",
-       "metric/test/group_by=test_name",
+       "metric/test/metric_group_by=test_name",
+       "metric/test/metric_group_by/test_name/metric_group_by_field=test_name",
+       "metric/test/metric_group_by/test_name/metric_group_by_method=foo",
+       "metric/test/metric_group_by/test_name/metric_group_by_method/foo/method=linear",
+       "metric/test/metric_group_by/test_name/metric_group_by_method/foo/metric_group_by_method_linear_max=100",
+       "metric/test/metric_group_by/test_name/metric_group_by_method/foo/metric_group_by_method_linear_step=10",
        NULL
 };
 
index 3eacf437985ced3e80a2e9c366af99d6bb418058..c2adf2edc6fde500b7d0236f2e8364184d1e1092 100644 (file)
@@ -132,7 +132,7 @@ static void test_stats_metrics_group_by_check_one(const struct metric *metric,
 
 #define DISCRETE_TEST_VAL_COUNT        3
 struct discrete_test {
-       const char *settings_blob;
+       const char *const *settings_blob;
        unsigned int num_values;
        const char *values_first[DISCRETE_TEST_VAL_COUNT];
        const char *values_second[DISCRETE_TEST_VAL_COUNT];
@@ -140,41 +140,64 @@ struct discrete_test {
 
 static const struct discrete_test discrete_tests[] = {
        {
-               "test_name sub_name",
+               (const char *const []){
+                 "metric/test/group_by=test_name,sub_name",
+                 "metric/test/group_by/test_name/field=test_name",
+                 "metric/test/group_by/sub_name/field=sub_name",
+                 NULL },
                3,
                { "eta", "kappa", "nu", },
                { "upsilon", "pi", "epsilon", },
        },
        {
-               "test_name:discrete sub_name:discrete",
+               (const char *const []){
+                 "metric/test/group_by=test_name,sub_name",
+                 "metric/test/group_by/test_name/field=test_name",
+                 "metric/test/group_by/test_name/method=discrete",
+                 "metric/test/group_by/test_name/method/discrete/method=discrete",
+                 "metric/test/group_by/sub_name/field=sub_name",
+                 "metric/test/group_by/sub_name/method=discrete",
+                 "metric/test/group_by/sub_name/method/discrete/method=discrete",
+                 NULL },
                3,
                { "apple", "bannana", "orange", },
                { "pie", "yoghurt", "cobbler", },
        },
        {
-               "test_name sub_name:discrete",
+               (const char *const []){
+                 "metric/test/group_by=test_name,sub_name",
+                 "metric/test/group_by/test_name/field=test_name",
+                 "metric/test/group_by/test_name/method/discrete/method=discrete",
+                 "metric/test/group_by/sub_name/field=sub_name",
+                 "metric/test/group_by/sub_name/method=discrete",
+                 "metric/test/group_by/sub_name/method/discrete/method=discrete",
+                 NULL },
                3,
                { "apollo", "gaia", "hermes", },
                { "thor", "odin", "loki", },
        },
 };
 
-static void test_stats_metrics_group_by_discrete_real(const struct discrete_test *test)
+static void
+test_stats_metrics_group_by_discrete_real(const struct discrete_test *test,
+                                         unsigned int group_idx)
 {
        struct event *event;
        unsigned int i, j;
 
-       test_begin(t_strdup_printf("stats metrics (discrete group by) - %s",
-                                  test->settings_blob));
+       test_begin(t_strdup_printf("stats metrics (discrete group by) - %u",
+                                  group_idx));
 
-       const char *const settings[] = {
+       const char *const base_settings[] = {
                "metric=test",
                "metric/test/metric_name=test",
                "metric/test/filter=event=test",
-               t_strdup_printf("metric/test/group_by=%s", test->settings_blob),
                NULL
        };
-       test_init(settings);
+       const char *const *all_settings = t_strsplit(
+               t_strconcat(t_strarray_join(base_settings, " "), " ",
+                           t_strarray_join(test->settings_blob, " "), NULL), " ");
+       test_init(all_settings);
 
        for (i = 0; i < test->num_values; i++) {
                for (j = 0; j < test->num_values; j++) {
@@ -235,12 +258,12 @@ static void test_stats_metrics_group_by_discrete(void)
        unsigned int i;
 
        for (i = 0; i < N_ELEMENTS(discrete_tests); i++)
-               test_stats_metrics_group_by_discrete_real(&discrete_tests[i]);
+               test_stats_metrics_group_by_discrete_real(&discrete_tests[i], i);
 }
 
 #define QUANTIZED_TEST_VAL_COUNT       15
 struct quantized_test {
-       const char *settings_blob;
+       const char *const *settings_blob;
        unsigned int num_inputs;
        intmax_t input_vals[QUANTIZED_TEST_VAL_COUNT];
 
@@ -255,7 +278,13 @@ struct quantized_test {
 
 static const struct quantized_test quantized_tests[] = {
        {
-               "linear:100:1000:100",
+               (const char *const []){
+                 "metric/test/group_by/foobar/method=linear",
+                 "metric/test/group_by/foobar/method/linear/method=linear",
+                 "metric/test/group_by/foobar/method/linear/min=100",
+                 "metric/test/group_by/foobar/method/linear/max=1000",
+                 "metric/test/group_by/foobar/method/linear/step=100",
+                 NULL },
                13,
                { 0, 50, 100, 101, 200, 201, 250, 301, 900, 901, 1000, 1001, 2000 },
                7,
@@ -275,7 +304,11 @@ static const struct quantized_test quantized_tests[] = {
        },
        {
                /* start at 0 */
-               "exponential:0:6:10",
+               (const char *const []){
+                 "metric/test/group_by/foobar/method=exponential",
+                 "metric/test/group_by/foobar/method/exponential/method=exponential",
+                 "metric/test/group_by/foobar/method/exponential/max_magnitude=6",
+                 NULL },
                12,
                { 0, 5, 10, 11, 100, 101, 500, 1000, 1001, 1000000, 1000001, 2000000 },
                7,
@@ -292,7 +325,12 @@ static const struct quantized_test quantized_tests[] = {
        },
        {
                /* start at 0 */
-               "exponential:0:6:2",
+               (const char *const []){
+                 "metric/test/group_by/foobar/method=exponential",
+                 "metric/test/group_by/foobar/method/exponential/method=exponential",
+                 "metric/test/group_by/foobar/method/exponential/max_magnitude=6",
+                 "metric/test/group_by/foobar/method/exponential/base=2",
+                 NULL },
                9,
                { 0, 1, 2, 4, 5, 20, 64, 65, 100 },
                7,
@@ -309,7 +347,12 @@ static const struct quantized_test quantized_tests[] = {
        },
        {
                /* start at >0 */
-               "exponential:2:6:10",
+               (const char *const []){
+                 "metric/test/group_by/foobar/method=exponential",
+                 "metric/test/group_by/foobar/method/exponential/method=exponential",
+                 "metric/test/group_by/foobar/method/exponential/min_magnitude=2",
+                 "metric/test/group_by/foobar/method/exponential/max_magnitude=6",
+                 NULL },
                12,
                { 0, 5, 10, 11, 100, 101, 500, 1000, 1001, 1000000, 1000001, 2000000 },
                5,
@@ -324,7 +367,13 @@ static const struct quantized_test quantized_tests[] = {
        },
        {
                /* start at >0 */
-               "exponential:2:6:2",
+               (const char *const []){
+                 "metric/test/group_by/foobar/method=exponential",
+                 "metric/test/group_by/foobar/method/exponential/method=exponential",
+                 "metric/test/group_by/foobar/method/exponential/min_magnitude=2",
+                 "metric/test/group_by/foobar/method/exponential/max_magnitude=6",
+                 "metric/test/group_by/foobar/method/exponential/base=2",
+                 NULL },
                9,
                { 0, 1, 2, 4, 5, 20, 64, 65, 100 },
                5,
@@ -339,22 +388,28 @@ static const struct quantized_test quantized_tests[] = {
        },
 };
 
-static void test_stats_metrics_group_by_quantized_real(const struct quantized_test *test)
+static void
+test_stats_metrics_group_by_quantized_real(const struct quantized_test *test,
+                                          unsigned int group_idx)
 {
        unsigned int i;
 
-       test_begin(t_strdup_printf("stats metrics (quantized group by) - %s",
-                                  test->settings_blob));
+       test_begin(t_strdup_printf("stats metrics (quantized group by) - %u",
+                                  group_idx));
 
-       const char *const settings[] = {
+       const char *const base_settings[] = {
                "metric=test",
                "metric/test/metric_name=test",
                "metric/test/filter=event=test",
-               t_strdup_printf("metric/test/group_by=test_name foobar:%s",
-                               test->settings_blob),
+               "metric/test/group_by=test_name,foobar",
+               "metric/test/group_by/test_name/field=test_name",
+               "metric/test/group_by/foobar/field=foobar",
                NULL
        };
-       test_init(settings);
+       const char *const *all_settings = t_strsplit(
+               t_strconcat(t_strarray_join(base_settings, " "), " ",
+                           t_strarray_join(test->settings_blob, " "), NULL), " ");
+       test_init(all_settings);
 
        struct event *event;
 
@@ -440,7 +495,7 @@ static void test_stats_metrics_group_by_quantized(void)
        unsigned int i;
 
        for (i = 0; i < N_ELEMENTS(quantized_tests); i++)
-               test_stats_metrics_group_by_quantized_real(&quantized_tests[i]);
+               test_stats_metrics_group_by_quantized_real(&quantized_tests[i], i);
 }
 
 int main(void) {