summaryrefslogtreecommitdiffstats
path: root/src/stats/client-writer.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/stats/client-writer.c')
-rw-r--r--src/stats/client-writer.c373
1 files changed, 373 insertions, 0 deletions
diff --git a/src/stats/client-writer.c b/src/stats/client-writer.c
new file mode 100644
index 0000000..40772cf
--- /dev/null
+++ b/src/stats/client-writer.c
@@ -0,0 +1,373 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "stats-common.h"
+#include "array.h"
+#include "llist.h"
+#include "hash.h"
+#include "str.h"
+#include "strescape.h"
+#include "lib-event-private.h"
+#include "event-filter.h"
+#include "ostream.h"
+#include "connection.h"
+#include "master-service.h"
+#include "stats-event-category.h"
+#include "stats-metrics.h"
+#include "stats-settings.h"
+#include "client-writer.h"
+
+#define STATS_UPDATE_CLIENTS_DELAY_MSECS 1000
+
+struct stats_event {
+ struct stats_event *prev, *next;
+
+ uint64_t id;
+ struct event *event;
+};
+
+struct writer_client {
+ struct connection conn;
+
+ struct stats_event *events;
+ HASH_TABLE(struct stats_event *, struct stats_event *) events_hash;
+};
+
+static struct timeout *to_update_clients;
+static struct connection_list *writer_clients = NULL;
+
+static void client_writer_send_handshake(struct writer_client *client)
+{
+ string_t *filter = t_str_new(128);
+ string_t *str = t_str_new(128);
+
+ event_filter_export(stats_metrics_get_event_filter(stats_metrics), filter);
+
+ str_append(str, "FILTER\t");
+ str_append_tabescaped(str, str_c(filter));
+ str_append_c(str, '\n');
+ o_stream_nsend(client->conn.output, str_data(str), str_len(str));
+}
+
+static unsigned int stats_event_hash(const struct stats_event *event)
+{
+ return (unsigned int)event->id;
+}
+
+static int stats_event_cmp(const struct stats_event *event1,
+ const struct stats_event *event2)
+{
+ return event1->id == event2->id ? 0 : 1;
+}
+
+void client_writer_create(int fd)
+{
+ struct writer_client *client;
+
+ client = i_new(struct writer_client, 1);
+ hash_table_create(&client->events_hash, default_pool, 0,
+ stats_event_hash, stats_event_cmp);
+
+ connection_init_server(writer_clients, &client->conn,
+ "stats", fd, fd);
+ client_writer_send_handshake(client);
+}
+
+static void writer_client_destroy(struct connection *conn)
+{
+ struct writer_client *client = (struct writer_client *)conn;
+ struct stats_event *event, *next;
+
+ for (event = client->events; event != NULL; event = next) {
+ next = event->next;
+ event_unref(&event->event);
+ i_free(event);
+ }
+ hash_table_destroy(&client->events_hash);
+
+ connection_deinit(conn);
+ i_free(conn);
+
+ master_service_client_connection_destroyed(master_service);
+}
+
+static struct stats_event *
+writer_client_find_event(struct writer_client *client, uint64_t event_id)
+{
+ struct stats_event lookup_event = { .id = event_id };
+ return hash_table_lookup(client->events_hash, &lookup_event);
+}
+
+static bool
+writer_client_run_event(struct writer_client *client,
+ uint64_t parent_event_id, const char *const *args,
+ struct event **event_r, const char **error_r)
+{
+ struct event *parent_event;
+ unsigned int log_type;
+
+ if (parent_event_id == 0)
+ parent_event = NULL;
+ else {
+ struct stats_event *stats_parent_event =
+ writer_client_find_event(client, parent_event_id);
+ if (stats_parent_event == NULL) {
+ *error_r = "Unknown parent event ID";
+ return FALSE;
+ }
+ parent_event = stats_parent_event->event;
+ }
+ if (args[0] == NULL || str_to_uint(args[0], &log_type) < 0 ||
+ log_type >= LOG_TYPE_COUNT) {
+ *error_r = "Invalid log type";
+ return FALSE;
+ }
+ const struct failure_context ctx = {
+ .type = (enum log_type)log_type
+ };
+ args++;
+
+ struct event *event = event_create(parent_event);
+ if (!event_import_unescaped(event, args, error_r)) {
+ event_unref(&event);
+ return FALSE;
+ }
+ stats_metrics_event(stats_metrics, event, &ctx);
+ *event_r = event;
+ return TRUE;
+}
+
+static bool
+writer_client_input_event(struct writer_client *client,
+ const char *const *args, const char **error_r)
+{
+ struct event *event, *global_event = NULL;
+ uint64_t parent_event_id, global_event_id;
+ bool ret;
+
+ if (args[1] == NULL || str_to_uint64(args[0], &global_event_id) < 0) {
+ *error_r = "Invalid global event ID";
+ return FALSE;
+ }
+ if (args[1] == NULL || str_to_uint64(args[1], &parent_event_id) < 0) {
+ *error_r = "Invalid parent ID";
+ return FALSE;
+ }
+
+ if (global_event_id != 0) {
+ struct stats_event *stats_global_event =
+ writer_client_find_event(client, global_event_id);
+ if (stats_global_event == NULL) {
+ *error_r = "Unknown global event ID";
+ return FALSE;
+ }
+ global_event = stats_global_event->event;
+ event_push_global(global_event);
+ }
+
+ ret = writer_client_run_event(client, parent_event_id, args+2,
+ &event, error_r);
+ if (global_event != NULL)
+ event_pop_global(global_event);
+ if (!ret)
+ return FALSE;
+ event_unref(&event);
+ return TRUE;
+}
+
+static bool
+writer_client_input_event_begin(struct writer_client *client,
+ const char *const *args, const char **error_r)
+{
+ struct event *event;
+ struct stats_event *stats_event;
+ uint64_t event_id, parent_event_id;
+
+ if (args[0] == NULL || args[1] == NULL ||
+ str_to_uint64(args[0], &event_id) < 0 ||
+ str_to_uint64(args[1], &parent_event_id) < 0) {
+ *error_r = "Invalid event IDs";
+ return FALSE;
+ }
+ if (writer_client_find_event(client, event_id) != NULL) {
+ *error_r = "Duplicate event ID";
+ return FALSE;
+ }
+ if (!writer_client_run_event(client, parent_event_id, args+2, &event, error_r))
+ return FALSE;
+
+ stats_event = i_new(struct stats_event, 1);
+ stats_event->id = event_id;
+ stats_event->event = event;
+ DLLIST_PREPEND(&client->events, stats_event);
+ hash_table_insert(client->events_hash, stats_event, stats_event);
+ return TRUE;
+}
+
+static bool
+writer_client_input_event_update(struct writer_client *client,
+ const char *const *args, const char **error_r)
+{
+ struct stats_event *stats_event, *parent_stats_event;
+ struct event *parent_event;
+ uint64_t event_id, parent_event_id;
+
+ if (args[0] == NULL || args[1] == NULL ||
+ str_to_uint64(args[0], &event_id) < 0 ||
+ str_to_uint64(args[1], &parent_event_id) < 0) {
+ *error_r = "Invalid event IDs";
+ return FALSE;
+ }
+ stats_event = writer_client_find_event(client, event_id);
+ if (stats_event == NULL) {
+ *error_r = "Unknown event ID";
+ return FALSE;
+ }
+ parent_stats_event = parent_event_id == 0 ? NULL :
+ writer_client_find_event(client, parent_event_id);
+ parent_event = parent_stats_event == NULL ? NULL :
+ parent_stats_event->event;
+ if (stats_event->event->parent != parent_event) {
+ *error_r = "Event unexpectedly changed parent";
+ return FALSE;
+ }
+ return event_import_unescaped(stats_event->event, args+2, error_r);
+}
+
+static bool
+writer_client_input_event_end(struct writer_client *client,
+ const char *const *args, const char **error_r)
+{
+ struct stats_event *stats_event;
+ uint64_t event_id;
+
+ if (args[0] == NULL || str_to_uint64(args[0], &event_id) < 0) {
+ *error_r = "Invalid event ID";
+ return FALSE;
+ }
+ stats_event = writer_client_find_event(client, event_id);
+ if (stats_event == NULL) {
+ *error_r = "Unknown event ID";
+ return FALSE;
+ }
+
+ DLLIST_REMOVE(&client->events, stats_event);
+ hash_table_remove(client->events_hash, stats_event);
+ event_unref(&stats_event->event);
+ i_free(stats_event);
+ return TRUE;
+}
+
+static bool
+writer_client_input_category(struct writer_client *client ATTR_UNUSED,
+ const char *const *args, const char **error_r)
+{
+ struct event_category *category, *parent;
+
+ if (args[0] == NULL) {
+ *error_r = "Missing category name";
+ return FALSE;
+ }
+ if (args[1] == NULL)
+ parent = NULL;
+ else if ((parent = event_category_find_registered(args[1])) == NULL) {
+ *error_r = "Unknown parent category";
+ return FALSE;
+ }
+
+ category = event_category_find_registered(args[0]);
+ if (category == NULL) {
+ /* new category - create */
+ stats_event_category_register(args[0], parent);
+ } else if (category->parent != parent) {
+ *error_r = t_strdup_printf(
+ "Category parent '%s' changed to '%s'",
+ category->parent == NULL ? "" : category->parent->name,
+ parent == NULL ? "" : parent->name);
+ return FALSE;
+ } else {
+ /* duplicate - ignore */
+ return TRUE;
+ }
+ return TRUE;
+}
+
+static int
+writer_client_input_args(struct connection *conn, const char *const *args)
+{
+ struct writer_client *client = (struct writer_client *)conn;
+ const char *error, *cmd = args[0];
+ bool ret;
+
+ if (cmd == NULL) {
+ i_error("Client sent empty line");
+ return 1;
+ }
+ if (strcmp(cmd, "EVENT") == 0)
+ ret = writer_client_input_event(client, args+1, &error);
+ else if (strcmp(cmd, "BEGIN") == 0)
+ ret = writer_client_input_event_begin(client, args+1, &error);
+ else if (strcmp(cmd, "UPDATE") == 0)
+ ret = writer_client_input_event_update(client, args+1, &error);
+ else if (strcmp(cmd, "END") == 0)
+ ret = writer_client_input_event_end(client, args+1, &error);
+ else if (strcmp(cmd, "CATEGORY") == 0)
+ ret = writer_client_input_category(client, args+1, &error);
+ else {
+ error = "Unknown command";
+ ret = FALSE;
+ }
+ if (!ret) {
+ i_error("Client sent invalid input for %s: %s (input: %s)",
+ cmd, error, t_strarray_join(args, "\t"));
+ return -1;
+ }
+ return 1;
+}
+
+static struct connection_settings client_set = {
+ .service_name_in = "stats-client",
+ .service_name_out = "stats-server",
+ .major_version = 4,
+ .minor_version = 0,
+
+ .input_max_size = 1024*128, /* "big enough" */
+ .output_max_size = SIZE_MAX,
+ .client = FALSE,
+};
+
+static const struct connection_vfuncs client_vfuncs = {
+ .destroy = writer_client_destroy,
+ .input_args = writer_client_input_args,
+};
+
+static void
+client_writer_update_connections_internal(void *context ATTR_UNUSED)
+{
+ struct connection *conn;
+ for (conn = writer_clients->connections; conn != NULL; conn = conn->next) {
+ struct writer_client *client =
+ container_of(conn, struct writer_client, conn);
+ client_writer_send_handshake(client);
+ }
+ timeout_remove(&to_update_clients);
+}
+
+void client_writer_update_connections(void)
+{
+ if (to_update_clients != NULL)
+ return;
+ to_update_clients = timeout_add(STATS_UPDATE_CLIENTS_DELAY_MSECS,
+ client_writer_update_connections_internal,
+ NULL);
+}
+
+void client_writers_init(void)
+{
+ writer_clients = connection_list_init(&client_set, &client_vfuncs);
+}
+
+void client_writers_deinit(void)
+{
+ timeout_remove(&to_update_clients);
+ connection_list_deinit(&writer_clients);
+}