pkglibexec_PROGRAMS = stats
+noinst_LTLIBRARIES = libstats_local.la
+
AM_CPPFLAGS = \
-I$(top_srcdir)/src/lib \
-I$(top_srcdir)/src/lib-settings \
-I$(top_srcdir)/src/lib-master \
-I$(top_srcdir)/src/lib-http \
-I$(top_srcdir)/src/lib-ssl-iostream \
+ -I$(top_srcdir)/src/lib-test \
$(BINARY_CFLAGS)
-stats_LDADD = $(LIBDOVECOT) \
+stats_LDADD = \
+ $(noinst_LTLIBRARIES) \
+ $(LIBDOVECOT) \
$(DOVECOT_SSL_LIBS) \
$(BINARY_LDFLAGS)
stats_DEPENDENCIES = \
+ $(noinst_LTLIBRARIES) \
$(DOVECOT_SSL_LIBS) \
$(LIBDOVECOT_DEPS)
stats_SOURCES = \
+ main.c
+
+libstats_local_la_SOURCES = \
client-reader.c \
client-writer.c \
event-exporter-fmt.c \
event-exporter-transport-drop.c \
event-exporter-transport-http-post.c \
event-exporter-transport-log.c \
- main.c \
stats-event-category.c \
stats-metrics.c \
stats-settings.c
event-exporter.h \
stats-event-category.h \
stats-metrics.h \
- stats-settings.h
+ stats-settings.h \
+ test-stats-common.h
+
+test_libs = \
+ $(noinst_LTLIBRARIES) \
+ $(DOVECOT_SSL_LIBS) \
+ $(LIBDOVECOT) \
+ $(BINARY_LDFLAGS)
+
+test_deps = \
+ $(noinst_LTLIBRARIES) \
+ $(DOVECOT_SSL_LIBS) \
+ $(LIBDOVECOT_DEPS)
+
+test_stats_metrics_SOURCES = test-stats-metrics.c test-stats-common.c
+test_stats_metrics_LDADD = $(test_libs)
+test_stats_metrics_DEPENDENCIES = $(test_deps)
+
+test_client_writer_SOURCES = test-client-writer.c test-stats-common.c
+test_client_writer_LDADD = $(test_libs)
+test_client_writer_DEPENDENCIES = $(test_deps)
+
+test_client_reader_SOURCES = test-client-reader.c test-stats-common.c
+test_client_reader_LDADD = $(test_libs)
+test_client_reader_DEPENDENCIES = $(test_deps)
+
+test_programs = test-stats-metrics test-client-writer test-client-reader
+noinst_PROGRAMS = $(test_programs)
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
+
+LIBDOVECOT_TEST_DEPS = \
+ ../lib-ssl-iostream/libssl_iostream.la \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+LIBDOVECOT_TEST = \
+ $(LIBDOVECOT_TEST_DEPS) \
+ $(MODULE_LIBS)
--- /dev/null
+/* Copyright (c) 2019 Dovecot authors, see the included COPYING file */
+
+#include "test-stats-common.h"
+#include "master-service-private.h"
+#include "client-reader.h"
+#include "connection.h"
+#include "ostream.h"
+
+static struct connection_list *conn_list;
+
+struct test_connection {
+ struct connection conn;
+
+ unsigned int row_count;
+};
+
+static void test_reader_server_destroy(struct connection *conn)
+{
+ io_loop_stop(conn->ioloop);
+}
+
+static struct connection_settings client_set = {
+ .service_name_in = "stats-reader-server",
+ .service_name_out = "stats-reader-client",
+ .major_version = 2,
+ .minor_version = 0,
+ .allow_empty_args_input = TRUE,
+ .input_max_size = (size_t)-1,
+ .output_max_size = (size_t)-1,
+ .client = TRUE,
+};
+
+bool test_stats_callback(struct event *event,
+ enum event_callback_type type ATTR_UNUSED,
+ struct failure_context *ctx, const char *fmt ATTR_UNUSED,
+ va_list args ATTR_UNUSED)
+{
+ if (metrics != NULL) {
+ stats_metrics_event(metrics, event, ctx);
+ struct event_filter *filter = stats_metrics_get_event_filter(metrics);
+ return !event_filter_match(filter, event, ctx);
+ }
+ return TRUE;
+}
+
+static const char *settings_blob_1 =
+"metric=test\n"
+"metric/test/name=test\n"
+"metric/test/event_name=test\n"
+"\n";
+
+static int test_reader_server_input_args(struct connection *conn ATTR_UNUSED,
+ const char *const *args)
+{
+ if (args[0] == NULL)
+ return -1;
+
+ test_assert_strcmp(args[0], "test");
+ test_assert_strcmp(args[1], "1");
+
+ return 1;
+}
+
+static void test_dump_metrics(void)
+{
+ int fds[2];
+
+ test_assert(socketpair(AF_UNIX, SOCK_STREAM, 0, fds) == 0);
+
+ struct connection *conn = i_new(struct connection, 1);
+
+ struct ioloop *loop = io_loop_create();
+
+ client_reader_create(fds[1], metrics);
+ connection_init_client_fd(conn_list, conn, "stats", fds[0], fds[0]);
+ o_stream_nsend_str(conn->output, "DUMP\tcount\n");
+
+ io_loop_run(loop);
+ connection_deinit(conn);
+ i_free(conn);
+
+ /* allow client-reader to finish up */
+ io_loop_set_running(loop);
+ io_loop_handler_run(loop);
+
+ io_loop_destroy(&loop);
+}
+
+static void test_client_reader(void)
+{
+ const struct connection_vfuncs client_vfuncs = {
+ .input_args = test_reader_server_input_args,
+ .destroy = test_reader_server_destroy,
+ };
+
+ test_begin("client reader");
+
+ /* register some stats */
+ test_init(settings_blob_1);
+
+ client_readers_init();
+ conn_list = connection_list_init(&client_set, &client_vfuncs);
+
+ /* push event in */
+ struct event *event = event_create(NULL);
+ event_add_category(event, &test_category);
+ event_set_name(event, "test");
+ test_event_send(event);
+ event_unref(&event);
+
+ test_assert(get_stats_dist_field("test", STATS_DIST_COUNT) == 1);
+ test_assert(get_stats_dist_field("test", STATS_DIST_SUM) > 0);
+
+ /* check output from reader */
+ test_dump_metrics();
+
+ test_deinit();
+
+ client_readers_deinit();
+ connection_list_deinit(&conn_list);
+
+ test_end();
+}
+
+int main(void) {
+ /* fake master service to pretend destroying
+ connections. */
+ struct master_service local_master_service = {
+ .stopping = TRUE,
+ .total_available_count = 100,
+ .service_count_left = 100,
+ };
+ void (*const test_functions[])(void) = {
+ test_client_reader,
+ NULL
+ };
+
+ master_service = &local_master_service;
+
+ int ret = test_run(test_functions);
+
+ return ret;
+}
--- /dev/null
+/* Copyright (c) 2019 Dovecot authors, see the included COPYING file */
+
+#include "test-stats-common.h"
+#include "master-service-private.h"
+#include "client-writer.h"
+#include "connection.h"
+#include "ostream.h"
+
+static struct event *last_sent_event = NULL;
+static bool recurse_back = FALSE;
+static struct connection_list *conn_list;
+
+static void test_writer_server_destroy(struct connection *conn)
+{
+ io_loop_stop(conn->ioloop);
+}
+
+static int test_writer_server_input_args(struct connection *conn,
+ const char *const *args ATTR_UNUSED)
+{
+ /* check filter */
+ test_assert_strcmp(args[0], "FILTER");
+ test_assert_strcmp(args[1], "ntest");
+ /* send commands now */
+ string_t *send_buf = t_str_new(128);
+ o_stream_nsend_str(conn->output, "CATEGORY\ttest\n");
+ str_printfa(send_buf, "BEGIN\t%"PRIu64"\t0\t0\t", last_sent_event->id);
+ event_export(last_sent_event, send_buf);
+ str_append_c(send_buf, '\n');
+ o_stream_nsend(conn->output, str_data(send_buf), str_len(send_buf));
+ str_truncate(send_buf, 0);
+ str_printfa(send_buf, "END\t%"PRIu64"\n", last_sent_event->id);
+ o_stream_nsend(conn->output, str_data(send_buf), str_len(send_buf));
+ /* disconnect immediately */
+ return -1;
+}
+
+static struct connection_settings client_set = {
+ .service_name_in = "stats-server",
+ .service_name_out = "stats-client",
+ .major_version = 3,
+ .minor_version = 0,
+
+ .input_max_size = (size_t)-1,
+ .output_max_size = (size_t)-1,
+ .client = TRUE,
+};
+
+static const struct connection_vfuncs client_vfuncs = {
+ .input_args = test_writer_server_input_args,
+ .destroy = test_writer_server_destroy,
+};
+
+static void test_write_one(struct event *event ATTR_UNUSED)
+{
+ int fds[2];
+
+ test_assert(socketpair(AF_UNIX, SOCK_STREAM, 0, fds) == 0);
+
+ struct connection *conn = i_new(struct connection, 1);
+
+ struct ioloop *loop = io_loop_create();
+
+ client_writer_create(fds[1], metrics);
+ connection_init_client_fd(conn_list, conn, "stats", fds[0], fds[0]);
+
+ last_sent_event = event;
+ io_loop_run(loop);
+ last_sent_event = NULL;
+ connection_deinit(conn);
+ i_free(conn);
+
+ /* client-writer needs two loops to deinit */
+ io_loop_set_running(loop);
+ io_loop_handler_run(loop);
+ io_loop_set_running(loop);
+ io_loop_handler_run(loop);
+
+ io_loop_destroy(&loop);
+}
+
+bool test_stats_callback(struct event *event,
+ enum event_callback_type type ATTR_UNUSED,
+ struct failure_context *ctx ATTR_UNUSED,
+ const char *fmt ATTR_UNUSED,
+ va_list args ATTR_UNUSED)
+{
+ if (recurse_back)
+ return TRUE;
+
+ recurse_back = TRUE;
+ if (metrics != NULL) {
+ test_write_one(event);
+ }
+ recurse_back = FALSE;
+
+ return TRUE;
+}
+
+static const char *settings_blob_1 =
+"metric=test\n"
+"metric/test/name=test\n"
+"metric/test/event_name=test\n"
+"\n";
+
+static void test_client_writer(void)
+{
+ test_begin("client writer");
+
+ /* register some stats */
+ test_init(settings_blob_1);
+
+ client_writers_init();
+ conn_list = connection_list_init(&client_set, &client_vfuncs);
+
+ /* push event in */
+ struct event *event = event_create(NULL);
+ event_add_category(event, &test_category);
+ event_set_name(event, "test");
+ test_event_send(event);
+ event_unref(&event);
+
+ test_assert(get_stats_dist_field("test", STATS_DIST_COUNT) == 1);
+ test_assert(get_stats_dist_field("test", STATS_DIST_SUM) > 0);
+
+ test_deinit();
+
+ client_writers_deinit();
+ connection_list_deinit(&conn_list);
+
+ test_end();
+}
+
+int main(void) {
+ /* fake master service to pretend destroying
+ connections. */
+ struct master_service local_master_service = {
+ .stopping = TRUE,
+ .total_available_count = 100,
+ .service_count_left = 100,
+ };
+ void (*const test_functions[])(void) = {
+ test_client_writer,
+ NULL
+ };
+
+ master_service = &local_master_service;
+
+ int ret = test_run(test_functions);
+
+ return ret;
+}
--- /dev/null
+/* Copyright (c) 2019 Dovecot authors, see the included COPYING file */
+
+#include "test-stats-common.h"
+
+struct event_category test_category = {
+ .name = "test",
+};
+
+struct event_category child_test_category = {
+ .name = "child",
+ .parent = &test_category,
+};
+
+pool_t test_pool;
+struct stats_metrics *metrics = NULL;
+
+static bool callback_added = FALSE;
+
+static struct stats_settings *read_settings(const char *settings)
+{
+ struct istream *is = test_istream_create(settings);
+ const char *error;
+ struct setting_parser_context *ctx =
+ settings_parser_init(test_pool, &stats_setting_parser_info, 0);
+ if (settings_parse_stream_read(ctx, is) < 0)
+ i_fatal("Failed to parse settings: %s",
+ settings_parser_get_error(ctx));
+ if (!settings_parser_check(ctx, test_pool, &error))
+ i_fatal("Failed to parse settings: %s",
+ error);
+ struct stats_settings *set = settings_parser_get(ctx);
+ settings_parser_deinit(&ctx);
+ i_stream_unref(&is);
+ return set;
+}
+
+void test_init(const char *settings_blob)
+{
+ if (!callback_added) {
+ event_register_callback(test_stats_callback);
+ callback_added = TRUE;
+ }
+
+ stats_event_categories_init();
+ test_pool = pool_alloconly_create(MEMPOOL_GROWING"test pool", 2048);
+
+ /* register test categories */
+ stats_event_category_register(test_category.name, NULL);
+ stats_event_category_register(child_test_category.name,
+ &test_category);
+ struct stats_settings *set = read_settings(settings_blob);
+ metrics = stats_metrics_init(set);
+}
+
+void test_deinit(void)
+{
+ stats_metrics_deinit(&metrics);
+ stats_event_categories_deinit();
+ pool_unref(&test_pool);
+}
+
+void test_event_send(struct event *event)
+{
+ struct failure_context ctx = {
+ .type = LOG_TYPE_DEBUG,
+ };
+
+ event_send(event, &ctx, "hello");
+}
+
+uint64_t get_stats_dist_field(const char *metric_name, enum stats_dist_field field)
+{
+ struct stats_metrics_iter *iter = stats_metrics_iterate_init(metrics);
+ const struct metric *metric;
+ while((metric = stats_metrics_iterate(iter)) != NULL)
+ if (strcmp(metric->name, metric_name) == 0)
+ break;
+
+ /* bug in test if not found */
+ i_assert(metric != NULL);
+
+ stats_metrics_iterate_deinit(&iter);
+
+ switch(field) {
+ case STATS_DIST_COUNT:
+ return stats_dist_get_count(metric->duration_stats);
+ case STATS_DIST_SUM:
+ return stats_dist_get_sum(metric->duration_stats);
+ default:
+ i_unreached();
+ }
+}
+
--- /dev/null
+#ifndef TEST_STATS_COMMON
+#define TEST_STATS_COMMON 1
+
+#include "lib.h"
+#include "event-filter.h"
+#include "istream.h"
+#include "settings-parser.h"
+#include "str.h"
+#include "test-common.h"
+#include "lib-event-private.h"
+#include "stats-dist.h"
+#include "stats-event-category.h"
+#include "stats-metrics.h"
+
+extern struct event_category test_category;
+extern struct event_category child_test_category;
+extern pool_t test_pool;
+
+extern struct stats_metrics *metrics;
+
+bool test_stats_callback(struct event *event,
+ enum event_callback_type type ATTR_UNUSED,
+ struct failure_context *ctx, const char *fmt ATTR_UNUSED,
+ va_list args ATTR_UNUSED);
+
+void test_init(const char *settings_blob);
+void test_deinit(void);
+
+void test_event_send(struct event *event);
+
+enum stats_dist_field {
+ STATS_DIST_COUNT,
+ STATS_DIST_SUM,
+};
+
+uint64_t get_stats_dist_field(const char *metric_name, enum stats_dist_field field);
+
+#endif
--- /dev/null
+/* Copyright (c) 2019 Dovecot authors, see the included COPYING file */
+
+#include "test-stats-common.h"
+
+bool test_stats_callback(struct event *event,
+ enum event_callback_type type ATTR_UNUSED,
+ struct failure_context *ctx, const char *fmt ATTR_UNUSED,
+ va_list args ATTR_UNUSED)
+{
+ if (metrics != NULL) {
+ stats_metrics_event(metrics, event, ctx);
+ struct event_filter *filter = stats_metrics_get_event_filter(metrics);
+ return !event_filter_match(filter, event, ctx);
+ }
+ return TRUE;
+}
+
+static const char *settings_blob_1 =
+"metric=test\n"
+"metric/test/name=test\n"
+"metric/test/event_name=test\n"
+"\n";
+
+static void test_stats_metrics(void)
+{
+ test_begin("stats metrics (event counting)");
+
+ /* register some stats */
+ test_init(settings_blob_1);
+
+ /* push event in */
+ struct event *event = event_create(NULL);
+ event_add_category(event, &test_category);
+ event_set_name(event, "test");
+ test_event_send(event);
+ event_unref(&event);
+
+ test_assert(get_stats_dist_field("test", STATS_DIST_COUNT) == 1);
+ test_assert(get_stats_dist_field("test", STATS_DIST_SUM) > 0);
+
+ test_deinit();
+ test_end();
+}
+
+static const char *settings_blob_2 =
+"metric=test\n"
+"metric/test/name=test\n"
+"metric/test/event_name=test\n"
+"metric/test/filter=\n"
+"metric/test/filter/test_field=value\n"
+"\n";
+
+static void test_stats_metrics_filter(void)
+{
+ test_begin("stats metrics (filter)");
+
+ test_init(settings_blob_2);
+
+ /* check filter */
+ struct event_filter *filter = stats_metrics_get_event_filter(metrics);
+ string_t *str_filter = t_str_new(64);
+ event_filter_export(filter, str_filter);
+ test_assert_strcmp("ntest ftest_field value ",
+ str_c(str_filter));
+
+ /* send event */
+ struct event *event = event_create(NULL);
+ event_add_category(event, &test_category);
+ event_set_name(event, "test");
+ event_add_str(event, "test_field", "value");
+ test_event_send(event);
+ event_unref(&event);
+
+ test_assert(get_stats_dist_field("test", STATS_DIST_COUNT) == 1);
+ test_assert(get_stats_dist_field("test", STATS_DIST_SUM) > 0);
+
+ /* send another event */
+ event = event_create(NULL);
+ event_add_category(event, &test_category);
+ event_set_name(event, "test");
+ event_add_str(event, "test_field", "nother value");
+ e_debug(event, "test");
+ event_unref(&event);
+
+ test_assert(get_stats_dist_field("test", STATS_DIST_COUNT) == 1);
+ test_assert(get_stats_dist_field("test", STATS_DIST_SUM) > 0);
+
+ test_deinit();
+ test_end();
+}
+
+int main(void) {
+ void (*const test_functions[])(void) = {
+ test_stats_metrics,
+ test_stats_metrics_filter,
+ NULL
+ };
+
+ int ret = test_run(test_functions);
+
+ return ret;
+}