]> git.ipfire.org Git - thirdparty/dovecot/core.git/commitdiff
lib-master: Add stats-client
authorTimo Sirainen <timo.sirainen@dovecot.fi>
Thu, 23 Nov 2017 16:46:23 +0000 (17:46 +0100)
committerTimo Sirainen <timo.sirainen@dovecot.fi>
Wed, 13 Dec 2017 11:22:17 +0000 (13:22 +0200)
Enable it if MASTER_SERVICE_FLAG_SEND_STATS is set

src/lib-master/Makefile.am
src/lib-master/master-service-private.h
src/lib-master/master-service-settings.c
src/lib-master/master-service-settings.h
src/lib-master/master-service.c
src/lib-master/master-service.h
src/lib-master/stats-client.c [new file with mode: 0644]
src/lib-master/stats-client.h [new file with mode: 0644]
src/lib/lib-event-private.h

index 24c613bcf46229fb74b2c52e55611c1efe71bbfb..43e4ee7a2afc4c76fd88c7df59c36d38bba545ac 100644 (file)
@@ -26,6 +26,7 @@ libmaster_la_SOURCES = \
        master-service-settings-cache.c \
        master-service-ssl.c \
        master-service-ssl-settings.c \
+       stats-client.c \
        syslog-util.c
 
 headers = \
@@ -44,6 +45,7 @@ headers = \
        master-service-ssl.h \
        master-service-ssl-settings.h \
        service-settings.h \
+       stats-client.h \
        syslog-util.h
 
 pkginc_libdir=$(pkgincludedir)
index b117b7c13f7d3d9936ea67e397df93bd52d434e8..10428082663042e533a4af8ce54033479394b9a5 100644 (file)
@@ -70,6 +70,7 @@ struct master_service {
        struct ssl_iostream_context *ssl_ctx;
        time_t ssl_params_last_refresh;
 
+       struct stats_client *stats_client;
        struct master_service_haproxy_conn *haproxy_conns;
 
        bool killed:1;
index 51ed220861ab407d8f98e0762a6bedc2f83e6d68..a2390ea7717f3016647aecf4124e551327117c61 100644 (file)
@@ -13,6 +13,7 @@
 #include "env-util.h"
 #include "execv-const.h"
 #include "settings-parser.h"
+#include "stats-client.h"
 #include "master-service-private.h"
 #include "master-service-ssl-settings.h"
 #include "master-service-settings.h"
@@ -45,6 +46,7 @@ static const struct setting_define master_service_setting_defines[] = {
        DEF(SET_STR, log_debug),
        DEF(SET_STR, syslog_facility),
        DEF(SET_STR, import_environment),
+       DEF(SET_STR, stats_writer_socket_path),
        DEF(SET_SIZE, config_cache_size),
        DEF(SET_BOOL, version_ignore),
        DEF(SET_BOOL, shutdown_clients),
@@ -79,6 +81,7 @@ static const struct master_service_settings master_service_default_settings = {
        .log_debug = "",
        .syslog_facility = "mail",
        .import_environment = "TZ CORE_OUTOFMEM CORE_ERROR" ENV_SYSTEMD ENV_GDB,
+       .stats_writer_socket_path = "stats-writer",
        .config_cache_size = 1024*1024,
        .version_ignore = FALSE,
        .shutdown_clients = TRUE,
@@ -580,6 +583,14 @@ int master_service_settings_read(struct master_service *service,
                /* running standalone. we want to ignore plugin versions. */
                service->version_string = NULL;
        }
+       if ((service->flags & MASTER_SERVICE_FLAG_SEND_STATS) != 0 &&
+           service->stats_client == NULL &&
+           service->set->stats_writer_socket_path[0] != '\0') T_BEGIN {
+               const char *path = t_strdup_printf("%s/%s",
+                       service->set->base_dir,
+                       service->set->stats_writer_socket_path);
+               service->stats_client = stats_client_init(path);
+       } T_END;
 
        if (service->set->shutdown_clients)
                master_service_set_die_with_master(master_service, TRUE);
index 4cd618213873d0c3e3af93f020289d131a447e73..2ae3b18f897f8c9fda7ed75dbbc679cf8a08430e 100644 (file)
@@ -17,6 +17,7 @@ struct master_service_settings {
        const char *log_debug;
        const char *syslog_facility;
        const char *import_environment;
+       const char *stats_writer_socket_path;
        uoff_t config_cache_size;
        bool version_ignore;
        bool shutdown_clients;
index cf26023bbec6eabfd1b9b72c386bc6a5a0c102f1..3bec33b0495fa4115624109aeb71126945434efd 100644 (file)
@@ -13,6 +13,7 @@
 #include "restrict-access.h"
 #include "settings-parser.h"
 #include "syslog-util.h"
+#include "stats-client.h"
 #include "master-instance.h"
 #include "master-login.h"
 #include "master-service-ssl.h"
@@ -282,7 +283,7 @@ master_service_init(const char *name, enum master_service_flags flags,
                        i_error("Invalid "DOVECOT_LOG_DEBUG_ENV" - ignoring: %s",
                                error);
                }
-               event_set_global_debug_filter(filter);
+               event_set_global_debug_log_filter(filter);
                event_filter_unref(&filter);
        }
 
@@ -981,6 +982,8 @@ void master_service_deinit(struct master_service **_service)
        master_service_io_listeners_remove(service);
        master_service_ssl_ctx_deinit(service);
 
+       if (service->stats_client != NULL)
+               stats_client_deinit(&service->stats_client);
        master_service_close_config_fd(service);
        timeout_remove(&service->to_die);
        timeout_remove(&service->to_overflow_state);
index 7cdcc387d385a2648448a6063a028fd4800a0eb3..ad07194cacdb836c98a33b3a220f5d7b3b87cca7 100644 (file)
@@ -36,7 +36,10 @@ enum master_service_flags {
        /* Don't create a data stack frame between master_service_init() and
           master_service_init_finish(). By default this is done to make sure
           initialization doesn't unnecessarily use up memory in data stack. */
-       MASTER_SERVICE_FLAG_NO_INIT_DATASTACK_FRAME = 0x800
+       MASTER_SERVICE_FLAG_NO_INIT_DATASTACK_FRAME = 0x800,
+       /* This process supports sending statistics to the stats process.
+          Connect to it at startup. */
+       MASTER_SERVICE_FLAG_SEND_STATS          = 0x1000
 };
 
 struct master_service_connection_proxy {
diff --git a/src/lib-master/stats-client.c b/src/lib-master/stats-client.c
new file mode 100644 (file)
index 0000000..8392c3d
--- /dev/null
@@ -0,0 +1,328 @@
+/* Copyright (c) 2017 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "strescape.h"
+#include "ostream.h"
+#include "time-util.h"
+#include "lib-event-private.h"
+#include "event-filter.h"
+#include "connection.h"
+#include "stats-client.h"
+
+#define STATS_CLIENT_RECONNECT_INTERVAL_MSECS (10*1000)
+
+struct stats_client {
+       struct connection conn;
+       struct event_filter *filter;
+       struct ioloop *ioloop;
+       struct timeout *to_reconnect;
+       bool handshaked;
+       bool handshake_received_at_least_once;
+};
+
+static struct connection_list *stats_clients;
+
+static void stats_client_connect(struct stats_client *client);
+
+static int
+stats_client_handshake(struct stats_client *client, const char *const *args)
+{
+       struct event_filter *filter;
+       const char *error;
+
+       filter = event_filter_create();
+       if (strcmp(args[0], "FILTER") != 0 ||
+           !event_filter_import_unescaped(filter, args+1, &error)) {
+               i_error("stats: Received invalid handshake: %s (input: %s)",
+                       error, t_strarray_join(args, "\t"));
+               event_filter_unref(&filter);
+               return -1;
+       }
+       client->handshaked = TRUE;
+       client->handshake_received_at_least_once = TRUE;
+       if (client->ioloop != NULL)
+               io_loop_stop(client->ioloop);
+
+       if (client->filter != NULL) {
+               /* Filter is already set. It becomes a bit complicated to
+                  change it. Since it's most likely exactly the same filter
+                  anyway, just keep the old one. */
+               event_filter_unref(&filter);
+               return 1;
+       }
+
+       client->filter = filter;
+       if (event_get_global_debug_send_filter() != NULL) {
+               /* merge into the global debug send filter */
+               event_filter_merge(event_get_global_debug_send_filter(),
+                                  client->filter);
+       } else {
+               /* no global filter yet - use this */
+               event_set_global_debug_send_filter(client->filter);
+       }
+       return 1;
+}
+
+static int
+stats_client_input_args(struct connection *conn, const char *const *args)
+{
+       struct stats_client *client = (struct stats_client *)conn;
+
+       if (!client->handshaked)
+               return stats_client_handshake(client, args);
+
+       i_error("stats: Received unexpected input: %s",
+               t_strarray_join(args, "\t"));
+       return 0;
+}
+
+static void stats_client_reconnect(struct stats_client *client)
+{
+       timeout_remove(&client->to_reconnect);
+       stats_client_connect(client);
+}
+
+static void stats_client_destroy(struct connection *conn)
+{
+       struct stats_client *client = (struct stats_client *)conn;
+       struct event *event;
+       unsigned int reconnect_msecs = STATS_CLIENT_RECONNECT_INTERVAL_MSECS;
+
+       /* after reconnection the IDs need to be re-sent */
+       for (event = events_get_head(); event != NULL; event = event->next)
+               event->id_sent_to_stats = FALSE;
+
+       client->handshaked = FALSE;
+       connection_disconnect(conn);
+       if (client->ioloop != NULL) {
+               /* waiting for stats handshake to finish */
+               io_loop_stop(client->ioloop);
+       } else if (conn->connect_finished.tv_sec != 0) {
+               int msecs_since_connected =
+                       timeval_diff_msecs(&ioloop_timeval,
+                                          &conn->connect_finished);
+               if (msecs_since_connected >= STATS_CLIENT_RECONNECT_INTERVAL_MSECS) {
+                       /* reconnect immdiately */
+                       reconnect_msecs = 0;
+               } else {
+                       /* wait for reconnect interval since we last
+                          were connected. */
+                       reconnect_msecs = STATS_CLIENT_RECONNECT_INTERVAL_MSECS -
+                               msecs_since_connected;
+               }
+       }
+       if (client->to_reconnect == NULL) {
+               client->to_reconnect =
+                       timeout_add(reconnect_msecs,
+                                   stats_client_reconnect, client);
+       }
+}
+
+static const struct connection_settings stats_client_set = {
+       .service_name_in = "stats-server",
+       .service_name_out = "stats-client",
+       .major_version = 2,
+       .minor_version = 0,
+
+       .input_max_size = (size_t)-1,
+       .output_max_size = (size_t)-1,
+       .client = TRUE
+};
+
+static const struct connection_vfuncs stats_client_vfuncs = {
+       .destroy = stats_client_destroy,
+       .input_args = stats_client_input_args,
+};
+
+static struct event *stats_event_get_parent(struct event *event)
+{
+       struct event *parent = event->parent;
+       unsigned int count;
+
+       if (parent == NULL || parent->id_sent_to_stats)
+               return parent;
+       /* avoid sending unnecessary events that don't add anything */
+       (void)event_get_fields(parent, &count);
+       if (count > 0)
+               return parent;
+       (void)event_get_categories(parent, &count);
+       if (count > 0)
+               return parent;
+       return stats_event_get_parent(parent);
+}
+
+static void stats_event_write(struct event *event, string_t *str, bool begin)
+{
+       struct event *parent_event =
+               begin ? event->parent : stats_event_get_parent(event);
+
+       /* FIXME: we could use create-timestamp of the events to figure out
+          whether to use BEGIN or to just merge the categories and fields
+          to the same EVENT. If the parent's timestamp is the same as ours,
+          don't bother using BEGIN for parent. */
+       if (parent_event != NULL) {
+               if (!parent_event->id_sent_to_stats)
+                       stats_event_write(parent_event, str, TRUE);
+       }
+       if (begin) {
+               str_printfa(str, "BEGIN\t%"PRIu64"\t", event->id);
+               event->id_sent_to_stats = TRUE;
+               event->call_free = TRUE;
+       } else {
+               str_append(str, "EVENT\t");
+       }
+       str_printfa(str, "%"PRIu64"\t",
+                   parent_event == NULL ? 0 : parent_event->id);
+       event_export(event, str);
+       str_append_c(str, '\n');
+}
+
+static void
+stats_client_send_event(struct stats_client *client, struct event *event)
+{
+       if (!client->handshaked || !event_filter_match(client->filter, event))
+               return;
+
+       string_t *str = t_str_new(256);
+       stats_event_write(event, str, FALSE);
+       o_stream_nsend(client->conn.output, str_data(str), str_len(str));
+}
+
+static void
+stats_client_free_event(struct stats_client *client, struct event *event)
+{
+       if (!event->id_sent_to_stats)
+               return;
+       o_stream_nsend_str(client->conn.output,
+                          t_strdup_printf("END\t%"PRIu64"\n", event->id));
+}
+
+static bool
+stats_event_callback(struct event *event, enum event_callback_type type,
+                    struct failure_context *ctx ATTR_UNUSED,
+                    const char *fmt ATTR_UNUSED, va_list args ATTR_UNUSED)
+{
+       if (stats_clients->connections == NULL)
+               return TRUE;
+       struct stats_client *client =
+               (struct stats_client *)stats_clients->connections;
+       if (client->conn.output == NULL)
+               return TRUE;
+
+       switch (type) {
+       case EVENT_CALLBACK_TYPE_EVENT:
+               stats_client_send_event(client, event);
+               break;
+       case EVENT_CALLBACK_TYPE_FREE:
+               stats_client_free_event(client, event);
+               break;
+       }
+       return TRUE;
+}
+
+static void
+stats_category_append(string_t *str, const struct event_category *category)
+{
+       str_append(str, "CATEGORY\t");
+       str_append_tabescaped(str, category->name);
+       if (category->parent != NULL) {
+               str_append_c(str, '\t');
+               str_append_tabescaped(str, category->parent->name);
+       }
+       str_append_c(str, '\n');
+}
+
+static void stats_category_registered(struct event_category *category)
+{
+       if (stats_clients->connections == NULL)
+               return;
+       struct stats_client *client =
+               (struct stats_client *)stats_clients->connections;
+       if (client->conn.output == NULL)
+               return;
+
+       string_t *str = t_str_new(64);
+       stats_category_append(str, category);
+       o_stream_nsend(client->conn.output, str_data(str), str_len(str));
+}
+
+static void stats_global_init(void)
+{
+       stats_clients = connection_list_init(&stats_client_set,
+                                            &stats_client_vfuncs);
+       event_register_callback(stats_event_callback);
+       event_category_register_callback(stats_category_registered);
+}
+
+static void stats_global_deinit(void)
+{
+       event_unregister_callback(stats_event_callback);
+       event_category_unregister_callback(stats_category_registered);
+       connection_list_deinit(&stats_clients);
+}
+
+static void stats_client_wait_handshake(struct stats_client *client)
+{
+       struct ioloop *prev_ioloop = current_ioloop;
+
+       client->ioloop = io_loop_create();
+       connection_switch_ioloop(&client->conn);
+       io_loop_run(client->ioloop);
+       io_loop_set_current(prev_ioloop);
+       connection_switch_ioloop(&client->conn);
+       io_loop_set_current(client->ioloop);
+       io_loop_destroy(&client->ioloop);
+}
+
+static void stats_client_send_registered_categories(struct stats_client *client)
+{
+       struct event_category *const *categories;
+       unsigned int i, count;
+
+       string_t *str = t_str_new(64);
+       categories = event_get_registered_categories(&count);
+       for (i = 0; i < count; i++)
+               stats_category_append(str, categories[i]);
+       o_stream_nsend(client->conn.output, str_data(str), str_len(str));
+}
+
+static void stats_client_connect(struct stats_client *client)
+{
+       if (connection_client_connect(&client->conn) < 0)
+               i_error("net_connect_unix(%s) failed: %m", client->conn.name);
+       else {
+               /* read the handshake so the global debug filter is updated */
+               stats_client_send_registered_categories(client);
+               if (!client->handshake_received_at_least_once)
+                       stats_client_wait_handshake(client);
+       }
+}
+
+struct stats_client *stats_client_init(const char *path)
+{
+       struct stats_client *client;
+
+       if (stats_clients == NULL)
+               stats_global_init();
+
+       client = i_new(struct stats_client, 1);
+       connection_init_client_unix(stats_clients, &client->conn, path);
+       stats_client_connect(client);
+       return client;
+}
+
+void stats_client_deinit(struct stats_client **_client)
+{
+       struct stats_client *client = *_client;
+
+       *_client = NULL;
+
+       event_filter_unref(&client->filter);
+       connection_deinit(&client->conn);
+       timeout_remove(&client->to_reconnect);
+       i_free(client);
+
+       if (stats_clients->connections == NULL)
+               stats_global_deinit();
+}
diff --git a/src/lib-master/stats-client.h b/src/lib-master/stats-client.h
new file mode 100644 (file)
index 0000000..fb0ca08
--- /dev/null
@@ -0,0 +1,7 @@
+#ifndef STATS_CLIENT_H
+#define STATS_CLIENT_H
+
+struct stats_client *stats_client_init(const char *path);
+void stats_client_deinit(struct stats_client **client);
+
+#endif
index c544c9202d07d27dd4d3ca06bbaf75f9e45d1def..ca41c92cb4a275d563b1c260af54af3f0c11581d 100644 (file)
@@ -18,6 +18,7 @@ struct event {
        bool forced_debug:1;
        bool always_log_source:1;
        bool sending_debug_log:1;
+       bool id_sent_to_stats:1;
        /* Call callbacks with EVENT_CALLBACK_TYPE_FREE for this event. */
        bool call_free:1;