1 /* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
2
3 #include "stats-common.h"
4 #include "array.h"
5 #include "str.h"
6 #include "str-sanitize.h"
7 #include "stats-dist.h"
8 #include "time-util.h"
9 #include "event-filter.h"
10 #include "event-exporter.h"
11 #include "stats-settings.h"
12 #include "stats-metrics.h"
13 #include "settings-parser.h"
14
15 #include <ctype.h>
16
17 #define LOG_EXPORTER_LONG_FIELD_TRUNCATE_LEN 1000
18
19 struct stats_metrics {
20 pool_t pool;
21 struct event_filter *filter; /* stats & export */
22 ARRAY(struct exporter *) exporters;
23 ARRAY(struct metric *) metrics;
24 };
25
26 static void
27 stats_metric_event(struct metric *metric, struct event *event, pool_t pool);
28 static struct metric *
29 stats_metric_sub_metric_alloc(struct metric *metric, const char *name, pool_t pool);
30
stats_exporters_add_set(struct stats_metrics * metrics,const struct stats_exporter_settings * set)31 static void stats_exporters_add_set(struct stats_metrics *metrics,
32 const struct stats_exporter_settings *set)
33 {
34 struct exporter *exporter;
35
36 exporter = p_new(metrics->pool, struct exporter, 1);
37 exporter->name = p_strdup(metrics->pool, set->name);
38 exporter->transport_args = p_strdup(metrics->pool, set->transport_args);
39 exporter->transport_timeout = set->transport_timeout;
40 exporter->time_format = set->parsed_time_format;
41
42 /* TODO: The following should be plugable.
43 *
44 * Note: Make sure to mirror any changes to the below code in
45 * stats_exporter_settings_check().
46 */
47 if (strcmp(set->format, "none") == 0) {
48 exporter->format = event_export_fmt_none;
49 exporter->format_mime_type = "application/octet-stream";
50 } else if (strcmp(set->format, "json") == 0) {
51 exporter->format = event_export_fmt_json;
52 exporter->format_mime_type = "application/json";
53 } else if (strcmp(set->format, "tab-text") == 0) {
54 exporter->format = event_export_fmt_tabescaped_text;
55 exporter->format_mime_type = "text/plain";
56 } else {
57 i_unreached();
58 }
59
60 /* TODO: The following should be plugable.
61 *
62 * Note: Make sure to mirror any changes to the below code in
63 * stats_exporter_settings_check().
64 */
65 if (strcmp(set->transport, "drop") == 0) {
66 exporter->transport = event_export_transport_drop;
67 } else if (strcmp(set->transport, "http-post") == 0) {
68 exporter->transport = event_export_transport_http_post;
69 } else if (strcmp(set->transport, "log") == 0) {
70 exporter->transport = event_export_transport_log;
71 exporter->format_max_field_len =
72 LOG_EXPORTER_LONG_FIELD_TRUNCATE_LEN;
73 } else {
74 i_unreached();
75 }
76
77 exporter->transport_args = set->transport_args;
78
79 array_push_back(&metrics->exporters, &exporter);
80 }
81
82 static struct metric *
stats_metric_alloc(pool_t pool,const char * name,const struct stats_metric_settings * set,const char * const * fields)83 stats_metric_alloc(pool_t pool, const char *name,
84 const struct stats_metric_settings *set,
85 const char *const *fields)
86 {
87 struct metric *metric = p_new(pool, struct metric, 1);
88 metric->name = p_strdup(pool, name);
89 metric->set = set;
90 metric->duration_stats = stats_dist_init();
91 metric->fields_count = str_array_length(fields);
92 if (metric->fields_count > 0) {
93 metric->fields = p_new(pool, struct metric_field,
94 metric->fields_count);
95 for (unsigned int i = 0; i < metric->fields_count; i++) {
96 metric->fields[i].field_key = p_strdup(pool, fields[i]);
97 metric->fields[i].stats = stats_dist_init();
98 }
99 }
100 return metric;
101 }
102
stats_metrics_add_set(struct stats_metrics * metrics,const struct stats_metric_settings * set)103 static void stats_metrics_add_set(struct stats_metrics *metrics,
104 const struct stats_metric_settings *set)
105 {
106 struct exporter *exporter;
107 struct metric *metric;
108 const char *const *fields;
109 const char *const *tmp;
110
111 fields = t_strsplit_spaces(set->fields, " ");
112 metric = stats_metric_alloc(metrics->pool, set->metric_name, set, fields);
113
114 if (array_is_created(&set->parsed_group_by))
115 metric->group_by = array_get(&set->parsed_group_by,
116 &metric->group_by_count);
117
118 array_push_back(&metrics->metrics, &metric);
119
120 event_filter_merge_with_context(metrics->filter, set->parsed_filter, metric);
121
122 /*
123 * Metrics may also be exported - make sure exporter info is set
124 */
125
126 if (set->exporter[0] == '\0')
127 return; /* not exported */
128
129 array_foreach_elem(&metrics->exporters, exporter) {
130 if (strcmp(set->exporter, exporter->name) == 0) {
131 metric->export_info.exporter = exporter;
132 break;
133 }
134 }
135
136 if (metric->export_info.exporter == NULL)
137 i_panic("Could not find exporter (%s) for metric (%s)",
138 set->exporter, set->metric_name);
139
140 /* Defaults */
141 metric->export_info.include = EVENT_EXPORTER_INCL_NONE;
142
143 tmp = t_strsplit_spaces(set->exporter_include, " ");
144 for (; *tmp != NULL; tmp++) {
145 if (strcmp(*tmp, "name") == 0)
146 metric->export_info.include |= EVENT_EXPORTER_INCL_NAME;
147 else if (strcmp(*tmp, "hostname") == 0)
148 metric->export_info.include |= EVENT_EXPORTER_INCL_HOSTNAME;
149 else if (strcmp(*tmp, "timestamps") == 0)
150 metric->export_info.include |= EVENT_EXPORTER_INCL_TIMESTAMPS;
151 else if (strcmp(*tmp, "categories") == 0)
152 metric->export_info.include |= EVENT_EXPORTER_INCL_CATEGORIES;
153 else if (strcmp(*tmp, "fields") == 0)
154 metric->export_info.include |= EVENT_EXPORTER_INCL_FIELDS;
155 else
156 i_warning("Ignoring unknown exporter include '%s'", *tmp);
157 }
158 }
159
160 static struct stats_metric_settings *
stats_metric_settings_dup(pool_t pool,const struct stats_metric_settings * src)161 stats_metric_settings_dup(pool_t pool, const struct stats_metric_settings *src)
162 {
163 struct stats_metric_settings *set = p_new(pool, struct stats_metric_settings, 1);
164
165 set->metric_name = p_strdup(pool, src->metric_name);
166 set->description = p_strdup(pool, src->description);
167 set->fields = p_strdup(pool, src->fields);
168 set->group_by = p_strdup(pool, src->group_by);
169 set->filter = p_strdup(pool, src->filter);
170 set->exporter = p_strdup(pool, src->exporter);
171 set->exporter_include = p_strdup(pool, src->exporter_include);
172
173 return set;
174 }
175
176 static struct metric *
stats_metrics_find(struct stats_metrics * metrics,const char * name,unsigned int * idx_r)177 stats_metrics_find(struct stats_metrics *metrics,
178 const char *name, unsigned int *idx_r)
179 {
180 struct metric *const *m;
181 array_foreach(&metrics->metrics, m) {
182 if (strcmp((*m)->name, name) == 0) {
183 *idx_r = array_foreach_idx(&metrics->metrics, m);
184 return *m;
185 }
186 }
187 return NULL;
188 }
189
stats_metrics_add_dynamic(struct stats_metrics * metrics,struct stats_metric_settings * set,const char ** error_r)190 bool stats_metrics_add_dynamic(struct stats_metrics *metrics,
191 struct stats_metric_settings *set,
192 const char **error_r)
193 {
194 unsigned int existing_idx ATTR_UNUSED;
195 if (stats_metrics_find(metrics, set->metric_name, &existing_idx) != NULL) {
196 *error_r = "Metric already exists";
197 return FALSE;
198 }
199
200 struct stats_metric_settings *_set =
201 stats_metric_settings_dup(metrics->pool, set);
202 if (!stats_metric_setting_parser_info.check_func(_set, metrics->pool, error_r))
203 return FALSE;
204 stats_metrics_add_set(metrics, _set);
205 return TRUE;
206 }
207
stats_metrics_remove_dynamic(struct stats_metrics * metrics,const char * name)208 bool stats_metrics_remove_dynamic(struct stats_metrics *metrics,
209 const char *name)
210 {
211 unsigned int m_idx;
212 struct metric *m = stats_metrics_find(metrics, name, &m_idx);
213 if (m != NULL) {
214 array_delete(&metrics->metrics, m_idx, 1);
215 return event_filter_remove_queries_with_context(metrics->filter, m);
216 }
217 return FALSE;
218 }
219
220 static void
stats_metrics_add_from_settings(struct stats_metrics * metrics,const struct stats_settings * set)221 stats_metrics_add_from_settings(struct stats_metrics *metrics,
222 const struct stats_settings *set)
223 {
224 /* add all the exporters first */
225 if (!array_is_created(&set->exporters)) {
226 p_array_init(&metrics->exporters, metrics->pool, 0);
227 } else {
228 struct stats_exporter_settings *exporter_set;
229
230 p_array_init(&metrics->exporters, metrics->pool,
231 array_count(&set->exporters));
232 array_foreach_elem(&set->exporters, exporter_set)
233 stats_exporters_add_set(metrics, exporter_set);
234 }
235
236 /* then add all the metrics */
237 if (!array_is_created(&set->metrics)) {
238 p_array_init(&metrics->metrics, metrics->pool, 0);
239 } else {
240 struct stats_metric_settings *metric_set;
241
242 p_array_init(&metrics->metrics, metrics->pool,
243 array_count(&set->metrics));
244 array_foreach_elem(&set->metrics, metric_set) T_BEGIN {
245 stats_metrics_add_set(metrics, metric_set);
246 } T_END;
247 }
248 }
249
stats_metrics_init(const struct stats_settings * set)250 struct stats_metrics *stats_metrics_init(const struct stats_settings *set)
251 {
252 struct stats_metrics *metrics;
253 pool_t pool = pool_alloconly_create("stats metrics", 1024);
254
255 metrics = p_new(pool, struct stats_metrics, 1);
256 metrics->pool = pool;
257 metrics->filter = event_filter_create();
258 stats_metrics_add_from_settings(metrics, set);
259 return metrics;
260 }
261
stats_metric_free(struct metric * metric)262 static void stats_metric_free(struct metric *metric)
263 {
264 struct metric *sub_metric;
265 stats_dist_deinit(&metric->duration_stats);
266 for (unsigned int i = 0; i < metric->fields_count; i++)
267 stats_dist_deinit(&metric->fields[i].stats);
268 if (!array_is_created(&metric->sub_metrics))
269 return;
270 array_foreach_elem(&metric->sub_metrics, sub_metric)
271 stats_metric_free(sub_metric);
272 }
273
stats_export_deinit(void)274 static void stats_export_deinit(void)
275 {
276 /* no need for event_export_transport_drop_deinit() - no-op */
277 event_export_transport_http_post_deinit();
278 /* no need for event_export_transport_log_deinit() - no-op */
279 }
280
stats_metrics_deinit(struct stats_metrics ** _metrics)281 void stats_metrics_deinit(struct stats_metrics **_metrics)
282 {
283 struct stats_metrics *metrics = *_metrics;
284 struct metric *metric;
285
286 *_metrics = NULL;
287
288 stats_export_deinit();
289
290 array_foreach_elem(&metrics->metrics, metric)
291 stats_metric_free(metric);
292 event_filter_unref(&metrics->filter);
293 pool_unref(&metrics->pool);
294 }
295
stats_metric_reset(struct metric * metric)296 static void stats_metric_reset(struct metric *metric)
297 {
298 struct metric *sub_metric;
299 stats_dist_reset(metric->duration_stats);
300 for (unsigned int i = 0; i < metric->fields_count; i++)
301 stats_dist_reset(metric->fields[i].stats);
302 if (!array_is_created(&metric->sub_metrics))
303 return;
304 array_foreach_elem(&metric->sub_metrics, sub_metric)
305 stats_metric_reset(sub_metric);
306 }
307
stats_metrics_reset(struct stats_metrics * metrics)308 void stats_metrics_reset(struct stats_metrics *metrics)
309 {
310 struct metric *metric;
311
312 array_foreach_elem(&metrics->metrics, metric)
313 stats_metric_reset(metric);
314 }
315
316 struct event_filter *
stats_metrics_get_event_filter(struct stats_metrics * metrics)317 stats_metrics_get_event_filter(struct stats_metrics *metrics)
318 {
319 return metrics->filter;
320 }
321
322 static struct metric *
stats_metric_get_sub_metric(struct metric * metric,const struct metric_value * value)323 stats_metric_get_sub_metric(struct metric *metric,
324 const struct metric_value *value)
325 {
326 struct metric *sub_metrics;
327
328 /* lookup sub-metric */
329 array_foreach_elem(&metric->sub_metrics, sub_metrics) {
330 switch (sub_metrics->group_value.type) {
331 case METRIC_VALUE_TYPE_STR:
332 if (memcmp(sub_metrics->group_value.hash, value->hash,
333 SHA1_RESULTLEN) == 0)
334 return sub_metrics;
335 break;
336 case METRIC_VALUE_TYPE_INT:
337 if (sub_metrics->group_value.intmax == value->intmax)
338 return sub_metrics;
339 break;
340 case METRIC_VALUE_TYPE_BUCKET_INDEX:
341 if (sub_metrics->group_value.intmax == value->intmax)
342 return sub_metrics;
343 break;
344 }
345 }
346 return NULL;
347 }
348
349 static struct metric *
stats_metric_sub_metric_alloc(struct metric * metric,const char * name,pool_t pool)350 stats_metric_sub_metric_alloc(struct metric *metric, const char *name, pool_t pool)
351 {
352 struct metric *sub_metric;
353 ARRAY_TYPE(const_string) fields;
354 t_array_init(&fields, metric->fields_count);
355 for (unsigned int i = 0; i < metric->fields_count; i++)
356 array_append(&fields, &metric->fields[i].field_key, 1);
357 array_append_zero(&fields);
358 sub_metric = stats_metric_alloc(pool, metric->name, metric->set,
359 array_idx(&fields, 0));
360 sub_metric->sub_name = p_strdup(pool, str_sanitize_utf8(name, 32));
361 array_append(&metric->sub_metrics, &sub_metric, 1);
362 return sub_metric;
363 }
364
365 static bool
stats_metric_group_by_discrete(const struct event_field * field,struct metric_value * value)366 stats_metric_group_by_discrete(const struct event_field *field,
367 struct metric_value *value)
368 {
369 switch (field->value_type) {
370 case EVENT_FIELD_VALUE_TYPE_STR:
371 value->type = METRIC_VALUE_TYPE_STR;
372 /* use sha1 of value to avoid excessive memory usage in case the
373 actual value is quite long */
374 sha1_get_digest(field->value.str, strlen(field->value.str),
375 value->hash);
376 return TRUE;
377 case EVENT_FIELD_VALUE_TYPE_INTMAX:
378 value->type = METRIC_VALUE_TYPE_INT;
379 value->intmax = field->value.intmax;
380 return TRUE;
381 case EVENT_FIELD_VALUE_TYPE_TIMEVAL:
382 return FALSE;
383 }
384
385 i_unreached();
386 }
387
388 /* convert the value to a bucket index */
389 static bool
stats_metric_group_by_quantized(const struct event_field * field,struct metric_value * value,const struct stats_metric_settings_group_by * group_by)390 stats_metric_group_by_quantized(const struct event_field *field,
391 struct metric_value *value,
392 const struct stats_metric_settings_group_by *group_by)
393 {
394 switch (field->value_type) {
395 case EVENT_FIELD_VALUE_TYPE_STR:
396 case EVENT_FIELD_VALUE_TYPE_TIMEVAL:
397 return FALSE;
398 case EVENT_FIELD_VALUE_TYPE_INTMAX:
399 break;
400 }
401
402 value->type = METRIC_VALUE_TYPE_BUCKET_INDEX;
403
404 for (unsigned int i = 0; i < group_by->num_ranges; i++) {
405 if ((field->value.intmax <= group_by->ranges[i].min) ||
406 (field->value.intmax > group_by->ranges[i].max))
407 continue;
408
409 value->intmax = i;
410 return TRUE;
411 }
412
413 i_panic("failed to find a matching bucket for '%s'=%jd",
414 group_by->field, field->value.intmax);
415 }
416
417 /* convert value to a bucket label */
418 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)419 stats_metric_group_by_quantized_label(const struct event_field *field,
420 const struct stats_metric_settings_group_by *group_by,
421 const size_t bucket_index)
422 {
423 const struct stats_metric_settings_bucket_range *range = &group_by->ranges[bucket_index];
424 const char *name = group_by->field;
425 const char *label;
426
427 switch (field->value_type) {
428 case EVENT_FIELD_VALUE_TYPE_STR:
429 case EVENT_FIELD_VALUE_TYPE_TIMEVAL:
430 i_unreached();
431 case EVENT_FIELD_VALUE_TYPE_INTMAX:
432 break;
433 }
434
435 if (range->min == INTMAX_MIN)
436 label = t_strdup_printf("%s_ninf_%jd", name, range->max);
437 else if (range->max == INTMAX_MAX)
438 label = t_strdup_printf("%s_%jd_inf", name, range->min + 1);
439 else
440 label = t_strdup_printf("%s_%jd_%jd", name,
441 range->min + 1, range->max);
442
443 return label;
444 }
445
446 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)447 stats_metric_group_by_get_value(const struct event_field *field,
448 const struct stats_metric_settings_group_by *group_by,
449 struct metric_value *value)
450 {
451 switch (group_by->func) {
452 case STATS_METRIC_GROUPBY_DISCRETE:
453 if (!stats_metric_group_by_discrete(field, value))
454 return FALSE;
455 return TRUE;
456 case STATS_METRIC_GROUPBY_QUANTIZED:
457 if (!stats_metric_group_by_quantized(field, value, group_by))
458 return FALSE;
459 return TRUE;
460 }
461
462 i_panic("unknown group-by function %d", group_by->func);
463 }
464
465 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)466 stats_metric_group_by_get_label(const struct event_field *field,
467 const struct stats_metric_settings_group_by *group_by,
468 const struct metric_value *value)
469 {
470 switch (group_by->func) {
471 case STATS_METRIC_GROUPBY_DISCRETE:
472 i_unreached();
473 case STATS_METRIC_GROUPBY_QUANTIZED:
474 return stats_metric_group_by_quantized_label(field, group_by,
475 value->intmax);
476 }
477
478 i_panic("unknown group-by function %d", group_by->func);
479 }
480
481 static void
stats_metric_group_by(struct metric * metric,struct event * event,pool_t pool)482 stats_metric_group_by(struct metric *metric, struct event *event, pool_t pool)
483 {
484 const struct stats_metric_settings_group_by *group_by = &metric->group_by[0];
485 const struct event_field *field =
486 event_find_field_recursive(event, group_by->field);
487 struct metric *sub_metric;
488 struct metric_value value;
489
490 /* ignore missing field */
491 if (field == NULL)
492 return;
493
494 if (!stats_metric_group_by_get_value(field, group_by, &value))
495 return;
496
497 if (!array_is_created(&metric->sub_metrics))
498 p_array_init(&metric->sub_metrics, pool, 8);
499
500 sub_metric = stats_metric_get_sub_metric(metric, &value);
501
502 if (sub_metric == NULL) T_BEGIN {
503 const char *value_label = NULL;
504
505 switch (value.type) {
506 case METRIC_VALUE_TYPE_STR:
507 value_label = field->value.str;
508 break;
509 case METRIC_VALUE_TYPE_INT:
510 value_label = dec2str(field->value.intmax);
511 break;
512 case METRIC_VALUE_TYPE_BUCKET_INDEX:
513 value_label = stats_metric_group_by_get_label(field,
514 group_by,
515 &value);
516 break;
517 }
518
519 sub_metric = stats_metric_sub_metric_alloc(metric, value_label,
520 pool);
521 if (metric->group_by_count > 1) {
522 sub_metric->group_by_count = metric->group_by_count - 1;
523 sub_metric->group_by = &metric->group_by[1];
524 }
525 sub_metric->group_value.type = value.type;
526 sub_metric->group_value.intmax = value.intmax;
527 memcpy(sub_metric->group_value.hash, value.hash, SHA1_RESULTLEN);
528 } T_END;
529
530 /* sub-metrics are recursive, so each sub-metric can have additional
531 sub-metrics. */
532 stats_metric_event(sub_metric, event, pool);
533 }
534
535 static void
stats_metric_event_field(struct event * event,const char * fieldname,struct stats_dist * stats)536 stats_metric_event_field(struct event *event, const char *fieldname,
537 struct stats_dist *stats)
538 {
539 const struct event_field *field =
540 event_find_field_recursive(event, fieldname);
541 intmax_t num = 0;
542
543 if (field == NULL)
544 return;
545
546 switch (field->value_type) {
547 case EVENT_FIELD_VALUE_TYPE_STR:
548 break;
549 case EVENT_FIELD_VALUE_TYPE_INTMAX:
550 num = field->value.intmax;
551 break;
552 case EVENT_FIELD_VALUE_TYPE_TIMEVAL:
553 num = field->value.timeval.tv_sec * 1000000ULL +
554 field->value.timeval.tv_usec;
555 break;
556 }
557
558 stats_dist_add(stats, num);
559 }
560
561 static void
stats_metric_event(struct metric * metric,struct event * event,pool_t pool)562 stats_metric_event(struct metric *metric, struct event *event, pool_t pool)
563 {
564 /* duration is special - we always add it */
565 stats_metric_event_field(event, STATS_EVENT_FIELD_NAME_DURATION,
566 metric->duration_stats);
567
568 for (unsigned int i = 0; i < metric->fields_count; i++)
569 stats_metric_event_field(event,
570 metric->fields[i].field_key,
571 metric->fields[i].stats);
572
573 if (metric->group_by != NULL)
574 stats_metric_group_by(metric, event, pool);
575 }
576
577 static void
stats_export_event(struct metric * metric,struct event * oldevent)578 stats_export_event(struct metric *metric, struct event *oldevent)
579 {
580 const struct metric_export_info *info = &metric->export_info;
581 const struct exporter *exporter = info->exporter;
582 struct event *event;
583
584 i_assert(exporter != NULL);
585
586 event = event_flatten(oldevent);
587
588 T_BEGIN {
589 buffer_t *buf;
590
591 buf = t_buffer_create(128);
592
593 exporter->format(metric, event, buf);
594 exporter->transport(exporter, buf);
595 } T_END;
596
597 event_unref(&event);
598 }
599
stats_metrics_event(struct stats_metrics * metrics,struct event * event,const struct failure_context * ctx)600 void stats_metrics_event(struct stats_metrics *metrics, struct event *event,
601 const struct failure_context *ctx)
602 {
603 struct event_filter_match_iter *iter;
604 struct metric *metric;
605 uintmax_t duration;
606
607 /* Note: Adding the field here means that it will get exported
608 below. This is necessary to allow group-by functions to quantize
609 based on the event duration. */
610 event_get_last_duration(event, &duration);
611 event_add_int(event, STATS_EVENT_FIELD_NAME_DURATION, duration);
612
613 /* process stats & exports */
614 iter = event_filter_match_iter_init(metrics->filter, event, ctx);
615 while ((metric = event_filter_match_iter_next(iter)) != NULL) T_BEGIN {
616 /* every metric is fed into stats */
617 stats_metric_event(metric, event, metrics->pool);
618
619 /* some metrics are exported */
620 if (metric->export_info.exporter != NULL)
621 stats_export_event(metric, event);
622 } T_END;
623 event_filter_match_iter_deinit(&iter);
624 }
625
626 struct stats_metrics_iter {
627 struct stats_metrics *metrics;
628 unsigned int idx;
629 };
630
631 struct stats_metrics_iter *
stats_metrics_iterate_init(struct stats_metrics * metrics)632 stats_metrics_iterate_init(struct stats_metrics *metrics)
633 {
634 struct stats_metrics_iter *iter;
635
636 iter = i_new(struct stats_metrics_iter, 1);
637 iter->metrics = metrics;
638 return iter;
639 }
640
stats_metrics_iterate(struct stats_metrics_iter * iter)641 const struct metric *stats_metrics_iterate(struct stats_metrics_iter *iter)
642 {
643 struct metric *const *metrics;
644 unsigned int count;
645
646 metrics = array_get(&iter->metrics->metrics, &count);
647 if (iter->idx >= count)
648 return NULL;
649 return metrics[iter->idx++];
650 }
651
stats_metrics_iterate_deinit(struct stats_metrics_iter ** _iter)652 void stats_metrics_iterate_deinit(struct stats_metrics_iter **_iter)
653 {
654 struct stats_metrics_iter *iter = *_iter;
655
656 *_iter = NULL;
657 i_free(iter);
658 }
659