/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */ #include "stats-common.h" #include "array.h" #include "str.h" #include "str-sanitize.h" #include "stats-dist.h" #include "time-util.h" #include "event-filter.h" #include "event-exporter.h" #include "stats-settings.h" #include "stats-metrics.h" #include "settings-parser.h" #include #define LOG_EXPORTER_LONG_FIELD_TRUNCATE_LEN 1000 struct stats_metrics { pool_t pool; struct event_filter *filter; /* stats & export */ ARRAY(struct exporter *) exporters; ARRAY(struct metric *) metrics; }; static void stats_metric_event(struct metric *metric, struct event *event, pool_t pool); static struct metric * stats_metric_sub_metric_alloc(struct metric *metric, const char *name, pool_t pool); static void stats_metric_free(struct metric *metric); static void stats_exporters_add_set(struct stats_metrics *metrics, const struct stats_exporter_settings *set) { struct exporter *exporter; exporter = p_new(metrics->pool, struct exporter, 1); exporter->name = p_strdup(metrics->pool, set->name); exporter->transport_args = p_strdup(metrics->pool, set->transport_args); exporter->transport_timeout = set->transport_timeout; exporter->time_format = set->parsed_time_format; /* TODO: The following should be plugable. * * Note: Make sure to mirror any changes to the below code in * stats_exporter_settings_check(). */ if (strcmp(set->format, "none") == 0) { exporter->format = event_export_fmt_none; exporter->format_mime_type = "application/octet-stream"; } else if (strcmp(set->format, "json") == 0) { exporter->format = event_export_fmt_json; exporter->format_mime_type = "application/json"; } else if (strcmp(set->format, "tab-text") == 0) { exporter->format = event_export_fmt_tabescaped_text; exporter->format_mime_type = "text/plain"; } else { i_unreached(); } /* TODO: The following should be plugable. * * Note: Make sure to mirror any changes to the below code in * stats_exporter_settings_check(). */ if (strcmp(set->transport, "drop") == 0) { exporter->transport = event_export_transport_drop; } else if (strcmp(set->transport, "http-post") == 0) { exporter->transport = event_export_transport_http_post; } else if (strcmp(set->transport, "log") == 0) { exporter->transport = event_export_transport_log; exporter->format_max_field_len = LOG_EXPORTER_LONG_FIELD_TRUNCATE_LEN; } else { i_unreached(); } exporter->transport_args = set->transport_args; array_push_back(&metrics->exporters, &exporter); } static struct metric * stats_metric_alloc(pool_t pool, const char *name, const struct stats_metric_settings *set, const char *const *fields) { struct metric *metric = p_new(pool, struct metric, 1); metric->name = p_strdup(pool, name); metric->set = set; metric->duration_stats = stats_dist_init(); metric->fields_count = str_array_length(fields); if (metric->fields_count > 0) { metric->fields = p_new(pool, struct metric_field, metric->fields_count); for (unsigned int i = 0; i < metric->fields_count; i++) { metric->fields[i].field_key = p_strdup(pool, fields[i]); metric->fields[i].stats = stats_dist_init(); } } return metric; } static void stats_metrics_add_set(struct stats_metrics *metrics, const struct stats_metric_settings *set) { struct exporter *exporter; struct metric *metric; const char *const *fields; const char *const *tmp; fields = t_strsplit_spaces(set->fields, " "); metric = stats_metric_alloc(metrics->pool, set->metric_name, set, fields); if (array_is_created(&set->parsed_group_by)) metric->group_by = array_get(&set->parsed_group_by, &metric->group_by_count); array_push_back(&metrics->metrics, &metric); event_filter_merge_with_context(metrics->filter, set->parsed_filter, metric); /* * Metrics may also be exported - make sure exporter info is set */ if (set->exporter[0] == '\0') return; /* not exported */ array_foreach_elem(&metrics->exporters, exporter) { if (strcmp(set->exporter, exporter->name) == 0) { metric->export_info.exporter = exporter; break; } } if (metric->export_info.exporter == NULL) i_panic("Could not find exporter (%s) for metric (%s)", set->exporter, set->metric_name); /* Defaults */ metric->export_info.include = EVENT_EXPORTER_INCL_NONE; tmp = t_strsplit_spaces(set->exporter_include, " "); for (; *tmp != NULL; tmp++) { if (strcmp(*tmp, "name") == 0) metric->export_info.include |= EVENT_EXPORTER_INCL_NAME; else if (strcmp(*tmp, "hostname") == 0) metric->export_info.include |= EVENT_EXPORTER_INCL_HOSTNAME; else if (strcmp(*tmp, "timestamps") == 0) metric->export_info.include |= EVENT_EXPORTER_INCL_TIMESTAMPS; else if (strcmp(*tmp, "categories") == 0) metric->export_info.include |= EVENT_EXPORTER_INCL_CATEGORIES; else if (strcmp(*tmp, "fields") == 0) metric->export_info.include |= EVENT_EXPORTER_INCL_FIELDS; else i_warning("Ignoring unknown exporter include '%s'", *tmp); } } static struct stats_metric_settings * stats_metric_settings_dup(pool_t pool, const struct stats_metric_settings *src) { struct stats_metric_settings *set = p_new(pool, struct stats_metric_settings, 1); set->metric_name = p_strdup(pool, src->metric_name); set->description = p_strdup(pool, src->description); set->fields = p_strdup(pool, src->fields); set->group_by = p_strdup(pool, src->group_by); set->filter = p_strdup(pool, src->filter); set->exporter = p_strdup(pool, src->exporter); set->exporter_include = p_strdup(pool, src->exporter_include); return set; } static struct metric * stats_metrics_find(struct stats_metrics *metrics, const char *name, unsigned int *idx_r) { struct metric *const *m; array_foreach(&metrics->metrics, m) { if (strcmp((*m)->name, name) == 0) { *idx_r = array_foreach_idx(&metrics->metrics, m); return *m; } } return NULL; } bool stats_metrics_add_dynamic(struct stats_metrics *metrics, struct stats_metric_settings *set, const char **error_r) { unsigned int existing_idx ATTR_UNUSED; if (stats_metrics_find(metrics, set->metric_name, &existing_idx) != NULL) { *error_r = "Metric already exists"; return FALSE; } struct stats_metric_settings *_set = stats_metric_settings_dup(metrics->pool, set); if (!stats_metric_setting_parser_info.check_func(_set, metrics->pool, error_r)) return FALSE; stats_metrics_add_set(metrics, _set); return TRUE; } bool stats_metrics_remove_dynamic(struct stats_metrics *metrics, const char *name) { unsigned int m_idx; bool ret = FALSE; struct metric *m = stats_metrics_find(metrics, name, &m_idx); if (m != NULL) { array_delete(&metrics->metrics, m_idx, 1); ret = event_filter_remove_queries_with_context(metrics->filter, m); stats_metric_free(m); } return ret; } static void stats_metrics_add_from_settings(struct stats_metrics *metrics, const struct stats_settings *set) { /* add all the exporters first */ if (!array_is_created(&set->exporters)) { p_array_init(&metrics->exporters, metrics->pool, 0); } else { struct stats_exporter_settings *exporter_set; p_array_init(&metrics->exporters, metrics->pool, array_count(&set->exporters)); array_foreach_elem(&set->exporters, exporter_set) stats_exporters_add_set(metrics, exporter_set); } /* then add all the metrics */ if (!array_is_created(&set->metrics)) { p_array_init(&metrics->metrics, metrics->pool, 0); } else { struct stats_metric_settings *metric_set; p_array_init(&metrics->metrics, metrics->pool, array_count(&set->metrics)); array_foreach_elem(&set->metrics, metric_set) T_BEGIN { stats_metrics_add_set(metrics, metric_set); } T_END; } } struct stats_metrics *stats_metrics_init(const struct stats_settings *set) { struct stats_metrics *metrics; pool_t pool = pool_alloconly_create("stats metrics", 1024); metrics = p_new(pool, struct stats_metrics, 1); metrics->pool = pool; metrics->filter = event_filter_create(); stats_metrics_add_from_settings(metrics, set); return metrics; } static void stats_metric_free(struct metric *metric) { struct metric *sub_metric; stats_dist_deinit(&metric->duration_stats); for (unsigned int i = 0; i < metric->fields_count; i++) stats_dist_deinit(&metric->fields[i].stats); if (!array_is_created(&metric->sub_metrics)) return; array_foreach_elem(&metric->sub_metrics, sub_metric) stats_metric_free(sub_metric); } static void stats_export_deinit(void) { /* no need for event_export_transport_drop_deinit() - no-op */ event_export_transport_http_post_deinit(); /* no need for event_export_transport_log_deinit() - no-op */ } void stats_metrics_deinit(struct stats_metrics **_metrics) { struct stats_metrics *metrics = *_metrics; struct metric *metric; *_metrics = NULL; stats_export_deinit(); array_foreach_elem(&metrics->metrics, metric) stats_metric_free(metric); event_filter_unref(&metrics->filter); pool_unref(&metrics->pool); } static void stats_metric_reset(struct metric *metric) { struct metric *sub_metric; stats_dist_reset(metric->duration_stats); for (unsigned int i = 0; i < metric->fields_count; i++) stats_dist_reset(metric->fields[i].stats); if (!array_is_created(&metric->sub_metrics)) return; array_foreach_elem(&metric->sub_metrics, sub_metric) stats_metric_reset(sub_metric); } void stats_metrics_reset(struct stats_metrics *metrics) { struct metric *metric; array_foreach_elem(&metrics->metrics, metric) stats_metric_reset(metric); } struct event_filter * stats_metrics_get_event_filter(struct stats_metrics *metrics) { return metrics->filter; } static struct metric * stats_metric_find_sub_metric(struct metric *metric, const struct metric_value *value) { struct metric *sub_metrics; /* lookup sub-metric */ array_foreach_elem(&metric->sub_metrics, sub_metrics) { switch (sub_metrics->group_value.type) { case METRIC_VALUE_TYPE_STR: if (memcmp(sub_metrics->group_value.hash, value->hash, SHA1_RESULTLEN) == 0) return sub_metrics; break; case METRIC_VALUE_TYPE_INT: if (sub_metrics->group_value.intmax == value->intmax) return sub_metrics; break; case METRIC_VALUE_TYPE_BUCKET_INDEX: if (sub_metrics->group_value.intmax == value->intmax) return sub_metrics; break; } } return NULL; } static struct metric * stats_metric_sub_metric_alloc(struct metric *metric, const char *name, pool_t pool) { struct metric *sub_metric; ARRAY_TYPE(const_string) fields; t_array_init(&fields, metric->fields_count); for (unsigned int i = 0; i < metric->fields_count; i++) array_append(&fields, &metric->fields[i].field_key, 1); array_append_zero(&fields); sub_metric = stats_metric_alloc(pool, metric->name, metric->set, array_idx(&fields, 0)); sub_metric->sub_name = p_strdup(pool, str_sanitize_utf8(name, 32)); array_append(&metric->sub_metrics, &sub_metric, 1); return sub_metric; } static bool stats_metric_group_by_discrete(const struct event_field *field, struct metric_value *value_r) { switch (field->value_type) { case EVENT_FIELD_VALUE_TYPE_STR: value_r->type = METRIC_VALUE_TYPE_STR; /* use sha1 of value to avoid excessive memory usage in case the actual value is quite long */ sha1_get_digest(field->value.str, strlen(field->value.str), value_r->hash); return TRUE; case EVENT_FIELD_VALUE_TYPE_INTMAX: value_r->type = METRIC_VALUE_TYPE_INT; value_r->intmax = field->value.intmax; return TRUE; case EVENT_FIELD_VALUE_TYPE_TIMEVAL: return FALSE; case EVENT_FIELD_VALUE_TYPE_STRLIST: return FALSE; } i_unreached(); } /* convert the value to a bucket index */ static bool stats_metric_group_by_quantized(const struct event_field *field, struct metric_value *value_r, const struct stats_metric_settings_group_by *group_by) { switch (field->value_type) { case EVENT_FIELD_VALUE_TYPE_STR: case EVENT_FIELD_VALUE_TYPE_TIMEVAL: case EVENT_FIELD_VALUE_TYPE_STRLIST: return FALSE; case EVENT_FIELD_VALUE_TYPE_INTMAX: break; } value_r->type = METRIC_VALUE_TYPE_BUCKET_INDEX; for (unsigned int i = 0; i < group_by->num_ranges; i++) { if ((field->value.intmax <= group_by->ranges[i].min) || (field->value.intmax > group_by->ranges[i].max)) continue; value_r->intmax = i; return TRUE; } i_panic("failed to find a matching bucket for '%s'=%jd", group_by->field, field->value.intmax); } /* convert value to a bucket label */ static const char * stats_metric_group_by_quantized_label(const struct event_field *field, const struct stats_metric_settings_group_by *group_by, const size_t bucket_index) { const struct stats_metric_settings_bucket_range *range = &group_by->ranges[bucket_index]; const char *name = group_by->field; const char *label; switch (field->value_type) { case EVENT_FIELD_VALUE_TYPE_STR: case EVENT_FIELD_VALUE_TYPE_TIMEVAL: case EVENT_FIELD_VALUE_TYPE_STRLIST: i_unreached(); case EVENT_FIELD_VALUE_TYPE_INTMAX: break; } if (range->min == INTMAX_MIN) label = t_strdup_printf("%s_ninf_%jd", name, range->max); else if (range->max == INTMAX_MAX) label = t_strdup_printf("%s_%jd_inf", name, range->min + 1); else label = t_strdup_printf("%s_%jd_%jd", name, range->min + 1, range->max); return label; } static bool stats_metric_group_by_get_value(const struct event_field *field, const struct stats_metric_settings_group_by *group_by, struct metric_value *value_r) { switch (group_by->func) { case STATS_METRIC_GROUPBY_DISCRETE: if (!stats_metric_group_by_discrete(field, value_r)) return FALSE; return TRUE; case STATS_METRIC_GROUPBY_QUANTIZED: if (!stats_metric_group_by_quantized(field, value_r, group_by)) return FALSE; return TRUE; } i_panic("unknown group-by function %d", group_by->func); } static const char * stats_metric_group_by_get_label(const struct event_field *field, const struct stats_metric_settings_group_by *group_by, const struct metric_value *value) { switch (group_by->func) { case STATS_METRIC_GROUPBY_DISCRETE: i_unreached(); case STATS_METRIC_GROUPBY_QUANTIZED: return stats_metric_group_by_quantized_label(field, group_by, value->intmax); } i_panic("unknown group-by function %d", group_by->func); } static const char * stats_metric_group_by_value_label(const struct event_field *field, const struct stats_metric_settings_group_by *group_by, const struct metric_value *value) { switch (value->type) { case METRIC_VALUE_TYPE_STR: return field->value.str; case METRIC_VALUE_TYPE_INT: return dec2str(field->value.intmax); case METRIC_VALUE_TYPE_BUCKET_INDEX: return stats_metric_group_by_get_label(field, group_by, value); } i_unreached(); } static struct metric * stats_metric_get_sub_metric(struct metric *metric, const struct event_field *field, const struct metric_value *value, pool_t pool) { struct metric *sub_metric; sub_metric = stats_metric_find_sub_metric(metric, value); if (sub_metric != NULL) return sub_metric; T_BEGIN { const char *value_label = stats_metric_group_by_value_label(field, &metric->group_by[0], value); sub_metric = stats_metric_sub_metric_alloc(metric, value_label, pool); } T_END; if (metric->group_by_count > 1) { sub_metric->group_by_count = metric->group_by_count - 1; sub_metric->group_by = &metric->group_by[1]; } sub_metric->group_value.type = value->type; sub_metric->group_value.intmax = value->intmax; memcpy(sub_metric->group_value.hash, value->hash, SHA1_RESULTLEN); return sub_metric; } static void stats_metric_group_by_field(struct metric *metric, struct event *event, const struct event_field *field, pool_t pool) { struct metric *sub_metric; struct metric_value value; if (!stats_metric_group_by_get_value(field, &metric->group_by[0], &value)) return; if (!array_is_created(&metric->sub_metrics)) p_array_init(&metric->sub_metrics, pool, 8); sub_metric = stats_metric_get_sub_metric(metric, field, &value, pool); /* sub-metrics are recursive, so each sub-metric can have additional sub-metrics. */ stats_metric_event(sub_metric, event, pool); } static void stats_event_get_strlist(struct event *event, const char *name, ARRAY_TYPE(const_string) *strings) { if (event == NULL) return; const struct event_field *field = event_find_field_nonrecursive(event, name); if (field != NULL) { const char *str; array_foreach_elem(&field->value.strlist, str) array_push_back(strings, &str); } stats_event_get_strlist(event_get_parent(event), name, strings); } static void stats_metric_group_by(struct metric *metric, struct event *event, pool_t pool) { const struct event_field *field = event_find_field_recursive(event, metric->group_by[0].field); /* ignore missing field */ if (field == NULL) return; if (field->value_type != EVENT_FIELD_VALUE_TYPE_STRLIST) stats_metric_group_by_field(metric, event, field, pool); else { /* Handle each string in strlist separately. The strlist needs to be combined from the event and its parents, as well as the global event and its parents. */ ARRAY_TYPE(const_string) strings; t_array_init(&strings, 8); stats_event_get_strlist(event, metric->group_by[0].field, &strings); stats_event_get_strlist(event_get_global(), metric->group_by[0].field, &strings); struct event_field str_field = { .value_type = EVENT_FIELD_VALUE_TYPE_STR, }; const char *str; /* sort strings so duplicates can be easily skipped */ array_sort(&strings, i_strcmp_p); array_foreach_elem(&strings, str) { if (str_field.value.str == NULL || strcmp(str_field.value.str, str) != 0) { str_field.value.str = str; stats_metric_group_by_field(metric, event, &str_field, pool); } } } } static void stats_metric_event_field(struct event *event, const char *fieldname, struct stats_dist *stats) { const struct event_field *field = event_find_field_recursive(event, fieldname); intmax_t num = 0; if (field == NULL) return; switch (field->value_type) { case EVENT_FIELD_VALUE_TYPE_STR: case EVENT_FIELD_VALUE_TYPE_STRLIST: break; case EVENT_FIELD_VALUE_TYPE_INTMAX: num = field->value.intmax; break; case EVENT_FIELD_VALUE_TYPE_TIMEVAL: num = field->value.timeval.tv_sec * 1000000ULL + field->value.timeval.tv_usec; break; } stats_dist_add(stats, num); } static void stats_metric_event(struct metric *metric, struct event *event, pool_t pool) { /* duration is special - we always add it */ stats_metric_event_field(event, STATS_EVENT_FIELD_NAME_DURATION, metric->duration_stats); for (unsigned int i = 0; i < metric->fields_count; i++) stats_metric_event_field(event, metric->fields[i].field_key, metric->fields[i].stats); if (metric->group_by != NULL) stats_metric_group_by(metric, event, pool); } static void stats_export_event(struct metric *metric, struct event *oldevent) { const struct metric_export_info *info = &metric->export_info; const struct exporter *exporter = info->exporter; struct event *event; i_assert(exporter != NULL); event = event_flatten(oldevent); T_BEGIN { buffer_t *buf; buf = t_buffer_create(128); exporter->format(metric, event, buf); exporter->transport(exporter, buf); } T_END; event_unref(&event); } void stats_metrics_event(struct stats_metrics *metrics, struct event *event, const struct failure_context *ctx) { struct event_filter_match_iter *iter; struct metric *metric; uintmax_t duration; /* Note: Adding the field here means that it will get exported below. This is necessary to allow group-by functions to quantize based on the event duration. */ event_get_last_duration(event, &duration); event_add_int(event, STATS_EVENT_FIELD_NAME_DURATION, duration); /* process stats & exports */ iter = event_filter_match_iter_init(metrics->filter, event, ctx); while ((metric = event_filter_match_iter_next(iter)) != NULL) T_BEGIN { /* every metric is fed into stats */ stats_metric_event(metric, event, metrics->pool); /* some metrics are exported */ if (metric->export_info.exporter != NULL) stats_export_event(metric, event); } T_END; event_filter_match_iter_deinit(&iter); } struct stats_metrics_iter { struct stats_metrics *metrics; unsigned int idx; }; struct stats_metrics_iter * stats_metrics_iterate_init(struct stats_metrics *metrics) { struct stats_metrics_iter *iter; iter = i_new(struct stats_metrics_iter, 1); iter->metrics = metrics; return iter; } const struct metric *stats_metrics_iterate(struct stats_metrics_iter *iter) { struct metric *const *metrics; unsigned int count; metrics = array_get(&iter->metrics->metrics, &count); if (iter->idx >= count) return NULL; return metrics[iter->idx++]; } void stats_metrics_iterate_deinit(struct stats_metrics_iter **_iter) { struct stats_metrics_iter *iter = *_iter; *_iter = NULL; i_free(iter); }