From: Timo Sirainen Date: Thu, 23 Nov 2017 16:46:23 +0000 (+0100) Subject: lib-master: Add stats-client X-Git-Tag: 2.3.0.rc1~85 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=b63e20ea9bc84f1aa90a551f217d01385e070b73;p=thirdparty%2Fdovecot%2Fcore.git lib-master: Add stats-client Enable it if MASTER_SERVICE_FLAG_SEND_STATS is set --- diff --git a/src/lib-master/Makefile.am b/src/lib-master/Makefile.am index 24c613bcf4..43e4ee7a2a 100644 --- a/src/lib-master/Makefile.am +++ b/src/lib-master/Makefile.am @@ -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) diff --git a/src/lib-master/master-service-private.h b/src/lib-master/master-service-private.h index b117b7c13f..1042808266 100644 --- a/src/lib-master/master-service-private.h +++ b/src/lib-master/master-service-private.h @@ -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; diff --git a/src/lib-master/master-service-settings.c b/src/lib-master/master-service-settings.c index 51ed220861..a2390ea771 100644 --- a/src/lib-master/master-service-settings.c +++ b/src/lib-master/master-service-settings.c @@ -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); diff --git a/src/lib-master/master-service-settings.h b/src/lib-master/master-service-settings.h index 4cd6182138..2ae3b18f89 100644 --- a/src/lib-master/master-service-settings.h +++ b/src/lib-master/master-service-settings.h @@ -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; diff --git a/src/lib-master/master-service.c b/src/lib-master/master-service.c index cf26023bbe..3bec33b049 100644 --- a/src/lib-master/master-service.c +++ b/src/lib-master/master-service.c @@ -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); diff --git a/src/lib-master/master-service.h b/src/lib-master/master-service.h index 7cdcc387d3..ad07194cac 100644 --- a/src/lib-master/master-service.h +++ b/src/lib-master/master-service.h @@ -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 index 0000000000..8392c3d38b --- /dev/null +++ b/src/lib-master/stats-client.c @@ -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 index 0000000000..fb0ca082b5 --- /dev/null +++ b/src/lib-master/stats-client.h @@ -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 diff --git a/src/lib/lib-event-private.h b/src/lib/lib-event-private.h index c544c9202d..ca41c92cb4 100644 --- a/src/lib/lib-event-private.h +++ b/src/lib/lib-event-private.h @@ -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;