diff --git a/Makefile.am b/Makefile.am index c008eb2de1..c22e174840 100644 --- a/Makefile.am +++ b/Makefile.am @@ -154,6 +154,8 @@ LIBNETDATA_FILES = \ libnetdata/dictionary/dictionary.h \ libnetdata/eval/eval.c \ libnetdata/eval/eval.h \ + libnetdata/facets/facets.c \ + libnetdata/facets/facets.h \ libnetdata/gorilla/gorilla.h \ libnetdata/gorilla/gorilla.cc \ libnetdata/inlined.h \ @@ -302,6 +304,11 @@ FREEIPMI_PLUGIN_FILES = \ $(LIBNETDATA_FILES) \ $(NULL) +SYSTEMD_JOURNAL_PLUGIN_FILES = \ + collectors/systemd-journal.plugin/systemd-journal.c \ + $(LIBNETDATA_FILES) \ + $(NULL) + CUPS_PLUGIN_FILES = \ collectors/cups.plugin/cups_plugin.c \ $(LIBNETDATA_FILES) \ @@ -1232,6 +1239,15 @@ if ENABLE_PLUGIN_FREEIPMI $(NULL) endif +if ENABLE_PLUGIN_SYSTEMD_JOURNAL + plugins_PROGRAMS += systemd-journal.plugin + systemd_journal_plugin_SOURCES = $(SYSTEMD_JOURNAL_PLUGIN_FILES) + systemd_journal_plugin_LDADD = \ + $(NETDATA_COMMON_LIBS) \ + $(OPTIONAL_SYSTEMD_LIBS) \ + $(NULL) +endif + if ENABLE_PLUGIN_EBPF plugins_PROGRAMS += ebpf.plugin ebpf_plugin_SOURCES = $(EBPF_PLUGIN_FILES) diff --git a/claim/claim.c b/claim/claim.c index c2b7b39562..d81440d2a1 100644 --- a/claim/claim.c +++ b/claim/claim.c @@ -356,7 +356,7 @@ int api_v2_claim(struct web_client *w, char *url) { BUFFER *wb = w->response.data; buffer_flush(wb); - buffer_json_initialize(wb, "\"", "\"", 0, true, false); + buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_DEFAULT); time_t now_s = now_realtime_sec(); CLOUD_STATUS status = buffer_json_cloud_status(wb, now_s); @@ -462,7 +462,7 @@ int api_v2_claim(struct web_client *w, char *url) { // our status may have changed // refresh the status in our output buffer_flush(wb); - buffer_json_initialize(wb, "\"", "\"", 0, true, false); + buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_DEFAULT); now_s = now_realtime_sec(); buffer_json_cloud_status(wb, now_s); diff --git a/collectors/Makefile.am b/collectors/Makefile.am index 2aec3dd3e8..d477e5b80e 100644 --- a/collectors/Makefile.am +++ b/collectors/Makefile.am @@ -25,6 +25,7 @@ SUBDIRS = \ statsd.plugin \ ebpf.plugin \ tc.plugin \ + systemd-journal.plugin \ $(NULL) usercustompluginsconfigdir=$(configdir)/custom-plugins.d diff --git a/collectors/apps.plugin/apps_plugin.c b/collectors/apps.plugin/apps_plugin.c index 631980dffd..f258a30ad0 100644 --- a/collectors/apps.plugin/apps_plugin.c +++ b/collectors/apps.plugin/apps_plugin.c @@ -13,7 +13,7 @@ #define APPS_PLUGIN_PROCESSES_FUNCTION_DESCRIPTION "Detailed information on the currently running processes." #define APPS_PLUGIN_FUNCTIONS() do { \ - fprintf(stdout, PLUGINSD_KEYWORD_FUNCTION " \"processes\" 10 \"%s\"\n", APPS_PLUGIN_PROCESSES_FUNCTION_DESCRIPTION); \ + fprintf(stdout, PLUGINSD_KEYWORD_FUNCTION " \"processes\" %d \"%s\"\n", PLUGINS_FUNCTIONS_TIMEOUT_DEFAULT, APPS_PLUGIN_PROCESSES_FUNCTION_DESCRIPTION); \ } while(0) @@ -4572,7 +4572,7 @@ static int check_capabilities() { } #endif -netdata_mutex_t mutex = NETDATA_MUTEX_INITIALIZER; +static netdata_mutex_t mutex = NETDATA_MUTEX_INITIALIZER; #define PROCESS_FILTER_CATEGORY "category:" #define PROCESS_FILTER_USER "user:" @@ -4625,15 +4625,6 @@ static void get_MemTotal(void) { #endif } -static void apps_plugin_function_error(const char *transaction, int code, const char *msg) { - char buffer[PLUGINSD_LINE_MAX + 1]; - json_escape_string(buffer, msg, PLUGINSD_LINE_MAX); - - pluginsd_function_result_begin_to_stdout(transaction, code, "application/json", now_realtime_sec()); - fprintf(stdout, "{\"status\":%d,\"error_message\":\"%s\"}", code, buffer); - pluginsd_function_result_end_to_stdout(); -} - static void apps_plugin_function_processes_help(const char *transaction) { pluginsd_function_result_begin_to_stdout(transaction, HTTP_RESP_OK, "text/plain", now_realtime_sec() + 3600); fprintf(stdout, "%s", @@ -4681,7 +4672,7 @@ static void apps_plugin_function_processes_help(const char *transaction) { buffer_json_add_array_item_double(wb, _tmp); \ } while(0) -static void apps_plugin_function_processes(const char *transaction, char *function __maybe_unused, char *line_buffer __maybe_unused, int line_max __maybe_unused, int timeout __maybe_unused) { +static void function_processes(const char *transaction, char *function __maybe_unused, char *line_buffer __maybe_unused, int line_max __maybe_unused, int timeout __maybe_unused) { struct pid_stat *p; char *words[PLUGINSD_MAX_WORDS] = { NULL }; @@ -4702,21 +4693,21 @@ static void apps_plugin_function_processes(const char *transaction, char *functi if(!category && strncmp(keyword, PROCESS_FILTER_CATEGORY, strlen(PROCESS_FILTER_CATEGORY)) == 0) { category = find_target_by_name(apps_groups_root_target, &keyword[strlen(PROCESS_FILTER_CATEGORY)]); if(!category) { - apps_plugin_function_error(transaction, HTTP_RESP_BAD_REQUEST, "No category with that name found."); + pluginsd_function_json_error(transaction, HTTP_RESP_BAD_REQUEST, "No category with that name found."); return; } } else if(!user && strncmp(keyword, PROCESS_FILTER_USER, strlen(PROCESS_FILTER_USER)) == 0) { user = find_target_by_name(users_root_target, &keyword[strlen(PROCESS_FILTER_USER)]); if(!user) { - apps_plugin_function_error(transaction, HTTP_RESP_BAD_REQUEST, "No user with that name found."); + pluginsd_function_json_error(transaction, HTTP_RESP_BAD_REQUEST, "No user with that name found."); return; } } else if(strncmp(keyword, PROCESS_FILTER_GROUP, strlen(PROCESS_FILTER_GROUP)) == 0) { group = find_target_by_name(groups_root_target, &keyword[strlen(PROCESS_FILTER_GROUP)]); if(!group) { - apps_plugin_function_error(transaction, HTTP_RESP_BAD_REQUEST, "No group with that name found."); + pluginsd_function_json_error(transaction, HTTP_RESP_BAD_REQUEST, "No group with that name found."); return; } } @@ -4742,7 +4733,7 @@ static void apps_plugin_function_processes(const char *transaction, char *functi else { char msg[PLUGINSD_LINE_MAX]; snprintfz(msg, PLUGINSD_LINE_MAX, "Invalid parameter '%s'", keyword); - apps_plugin_function_error(transaction, HTTP_RESP_BAD_REQUEST, msg); + pluginsd_function_json_error(transaction, HTTP_RESP_BAD_REQUEST, msg); return; } } @@ -4755,7 +4746,7 @@ static void apps_plugin_function_processes(const char *transaction, char *functi unsigned int io_divisor = 1024 * RATES_DETAIL; BUFFER *wb = buffer_create(PLUGINSD_LINE_MAX, NULL); - buffer_json_initialize(wb, "\"", "\"", 0, true, false); + buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_NEWLINE_ON_ARRAYS); buffer_json_member_add_uint64(wb, "status", HTTP_RESP_OK); buffer_json_member_add_string(wb, "type", "table"); buffer_json_member_add_time_t(wb, "update_every", update_every); @@ -5232,7 +5223,7 @@ static void apps_plugin_function_processes(const char *transaction, char *functi RRDF_FIELD_FILTER_RANGE, RRDF_FIELD_OPTS_VISIBLE, NULL); buffer_rrdf_table_add_field(wb, field_id++, "Uptime", "Uptime in seconds", RRDF_FIELD_TYPE_DURATION, - RRDF_FIELD_VISUAL_BAR, RRDF_FIELD_TRANSFORM_DURATION, 2, + RRDF_FIELD_VISUAL_BAR, RRDF_FIELD_TRANSFORM_DURATION_S, 2, "seconds", Uptime_max, RRDF_FIELD_SORT_DESCENDING, NULL, RRDF_FIELD_SUMMARY_MAX, RRDF_FIELD_FILTER_RANGE, RRDF_FIELD_OPTS_VISIBLE, NULL); @@ -5532,9 +5523,9 @@ static void apps_plugin_function_processes(const char *transaction, char *functi pluginsd_function_result_end_to_stdout(); } -bool apps_plugin_exit = false; +static bool apps_plugin_exit = false; -void *reader_main(void *arg __maybe_unused) { +static void *reader_main(void *arg __maybe_unused) { char buffer[PLUGINSD_LINE_MAX + 1]; char *s = NULL; @@ -5566,9 +5557,9 @@ void *reader_main(void *arg __maybe_unused) { netdata_mutex_lock(&mutex); if(strncmp(function, "processes", strlen("processes")) == 0) - apps_plugin_function_processes(transaction, function, buffer, PLUGINSD_LINE_MAX + 1, timeout); + function_processes(transaction, function, buffer, PLUGINSD_LINE_MAX + 1, timeout); else - apps_plugin_function_error(transaction, HTTP_RESP_NOT_FOUND, "No function with this name found in apps.plugin."); + pluginsd_function_json_error(transaction, HTTP_RESP_NOT_FOUND, "No function with this name found in apps.plugin."); fflush(stdout); netdata_mutex_unlock(&mutex); @@ -5696,6 +5687,8 @@ int main(int argc, char **argv) { netdata_thread_create(&reader_thread, "APPS_READER", NETDATA_THREAD_OPTION_DONT_LOG, reader_main, NULL); netdata_mutex_lock(&mutex); + APPS_PLUGIN_FUNCTIONS(); + usec_t step = update_every * USEC_PER_SEC; global_iterations_counter = 1; heartbeat_t hb; diff --git a/collectors/ebpf.plugin/ebpf_functions.c b/collectors/ebpf.plugin/ebpf_functions.c index cc26044c46..8f0244cde4 100644 --- a/collectors/ebpf.plugin/ebpf_functions.c +++ b/collectors/ebpf.plugin/ebpf_functions.c @@ -206,7 +206,7 @@ static void ebpf_function_thread_manipulation(const char *transaction, time_t expires = now_realtime_sec() + em->update_every; BUFFER *wb = buffer_create(PLUGINSD_LINE_MAX, NULL); - buffer_json_initialize(wb, "\"", "\"", 0, true, false); + buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_NEWLINE_ON_ARRAYS); buffer_json_member_add_uint64(wb, "status", HTTP_RESP_OK); buffer_json_member_add_string(wb, "type", "table"); buffer_json_member_add_time_t(wb, "update_every", em->update_every); diff --git a/collectors/plugins.d/plugins_d.h b/collectors/plugins.d/plugins_d.h index 1e183c2dc8..4988b50719 100644 --- a/collectors/plugins.d/plugins_d.h +++ b/collectors/plugins.d/plugins_d.h @@ -99,8 +99,6 @@ void pluginsd_process_thread_cleanup(void *ptr); size_t pluginsd_initialize_plugin_directories(); - - #define pluginsd_function_result_begin_to_buffer(wb, transaction, code, content_type, expires) \ buffer_sprintf(wb \ , PLUGINSD_KEYWORD_FUNCTION_RESULT_BEGIN " \"%s\" %d \"%s\" %ld\n" \ @@ -125,4 +123,13 @@ size_t pluginsd_initialize_plugin_directories(); #define pluginsd_function_result_end_to_stdout() \ fprintf(stdout, "\n" PLUGINSD_KEYWORD_FUNCTION_RESULT_END "\n") +static inline void pluginsd_function_json_error(const char *transaction, int code, const char *msg) { + char buffer[PLUGINSD_LINE_MAX + 1]; + json_escape_string(buffer, msg, PLUGINSD_LINE_MAX); + + pluginsd_function_result_begin_to_stdout(transaction, code, "application/json", now_realtime_sec()); + fprintf(stdout, "{\"status\":%d,\"error_message\":\"%s\"}", code, buffer); + pluginsd_function_result_end_to_stdout(); +} + #endif /* NETDATA_PLUGINS_D_H */ diff --git a/collectors/systemd-journal.plugin/Makefile.am b/collectors/systemd-journal.plugin/Makefile.am new file mode 100644 index 0000000000..e69de29bb2 diff --git a/collectors/systemd-journal.plugin/README.md b/collectors/systemd-journal.plugin/README.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/collectors/systemd-journal.plugin/systemd-journal.c b/collectors/systemd-journal.plugin/systemd-journal.c new file mode 100644 index 0000000000..95954355df --- /dev/null +++ b/collectors/systemd-journal.plugin/systemd-journal.c @@ -0,0 +1,578 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +/* + * netdata systemd-journal.plugin + * Copyright (C) 2023 Netdata Inc. + * GPL v3+ + */ + +// TODO - 1) MARKDOC + +#include "collectors/all.h" +#include "libnetdata/libnetdata.h" +#include "libnetdata/required_dummies.h" + +#include <systemd/sd-journal.h> +#include <syslog.h> + +#define FACET_MAX_VALUE_LENGTH 8192 + +#define SYSTEMD_JOURNAL_FUNCTION_DESCRIPTION "View, search and analyze systemd journal entries." +#define SYSTEMD_JOURNAL_FUNCTION_NAME "systemd-journal" +#define SYSTEMD_JOURNAL_DEFAULT_TIMEOUT 30 +#define SYSTEMD_JOURNAL_MAX_PARAMS 100 +#define SYSTEMD_JOURNAL_DEFAULT_QUERY_DURATION (3 * 3600) +#define SYSTEMD_JOURNAL_DEFAULT_ITEMS_PER_QUERY 200 + +#define JOURNAL_PARAMETER_HELP "help" +#define JOURNAL_PARAMETER_AFTER "after" +#define JOURNAL_PARAMETER_BEFORE "before" +#define JOURNAL_PARAMETER_ANCHOR "anchor" +#define JOURNAL_PARAMETER_LAST "last" +#define JOURNAL_PARAMETER_QUERY "query" + +#define SYSTEMD_ALWAYS_VISIBLE_KEYS NULL +#define SYSTEMD_KEYS_EXCLUDED_FROM_FACETS NULL +#define SYSTEMD_KEYS_INCLUDED_IN_FACETS \ + "_TRANSPORT" \ + "|SYSLOG_IDENTIFIER" \ + "|SYSLOG_FACILITY" \ + "|PRIORITY" \ + "|_HOSTNAME" \ + "|_RUNTIME_SCOPE" \ + "|_PID" \ + "|_UID" \ + "|_GID" \ + "|_SYSTEMD_UNIT" \ + "|_SYSTEMD_SLICE" \ + "|_SYSTEMD_USER_SLICE" \ + "|_COMM" \ + "|_EXE" \ + "|_SYSTEMD_CGROUP" \ + "|_SYSTEMD_USER_UNIT" \ + "|USER_UNIT" \ + "|UNIT" \ + "" + +static netdata_mutex_t mutex = NETDATA_MUTEX_INITIALIZER; +static bool plugin_should_exit = false; + +DICTIONARY *uids = NULL; +DICTIONARY *gids = NULL; + + +// ---------------------------------------------------------------------------- + +int systemd_journal_query(BUFFER *wb, FACETS *facets, usec_t after_ut, usec_t before_ut, usec_t stop_monotonic_ut) { + sd_journal *j; + int r; + + // Open the system journal for reading + r = sd_journal_open(&j, SD_JOURNAL_ALL_NAMESPACES); + if (r < 0) + return HTTP_RESP_INTERNAL_SERVER_ERROR; + + facets_rows_begin(facets); + + bool timed_out = false; + size_t row_counter = 0; + sd_journal_seek_realtime_usec(j, before_ut); + SD_JOURNAL_FOREACH_BACKWARDS(j) { + row_counter++; + + uint64_t msg_ut; + sd_journal_get_realtime_usec(j, &msg_ut); + if (msg_ut < after_ut) + break; + + const void *data; + size_t length; + SD_JOURNAL_FOREACH_DATA(j, data, length) { + const char *key = data; + const char *equal = strchr(key, '='); + if(unlikely(!equal)) + continue; + + const char *value = ++equal; + size_t key_length = value - key; // including '\0' + + char key_copy[key_length]; + memcpy(key_copy, key, key_length - 1); + key_copy[key_length - 1] = '\0'; + + size_t value_length = length - key_length; // without '\0' + facets_add_key_value_length(facets, key_copy, value, value_length <= FACET_MAX_VALUE_LENGTH ? value_length : FACET_MAX_VALUE_LENGTH); + } + + facets_row_finished(facets, msg_ut); + + if((row_counter % 100) == 0 && now_monotonic_usec() > stop_monotonic_ut) { + timed_out = true; + break; + } + } + + sd_journal_close(j); + + buffer_json_member_add_uint64(wb, "status", HTTP_RESP_OK); + buffer_json_member_add_boolean(wb, "partial", timed_out); + buffer_json_member_add_string(wb, "type", "table"); + buffer_json_member_add_time_t(wb, "update_every", 1); + buffer_json_member_add_string(wb, "help", SYSTEMD_JOURNAL_FUNCTION_DESCRIPTION); + + facets_report(facets, wb); + + buffer_json_member_add_time_t(wb, "expires", now_realtime_sec()); + buffer_json_finalize(wb); + + return HTTP_RESP_OK; +} + +static void systemd_journal_function_help(const char *transaction) { + pluginsd_function_result_begin_to_stdout(transaction, HTTP_RESP_OK, "text/plain", now_realtime_sec() + 3600); + fprintf(stdout, + "%s / %s\n" + "\n" + "%s\n" + "\n" + "The following filters are supported:\n" + "\n" + " help\n" + " Shows this help message.\n" + "\n" + " before:TIMESTAMP\n" + " Absolute or relative (to now) timestamp in seconds, to start the query.\n" + " The query is always executed from the most recent to the oldest log entry.\n" + " If not given the default is: now.\n" + "\n" + " after:TIMESTAMP\n" + " Absolute or relative (to `before`) timestamp in seconds, to end the query.\n" + " If not given, the default is %d.\n" + "\n" + " last:ITEMS\n" + " The number of items to return.\n" + " The default is %d.\n" + "\n" + " anchor:NUMBER\n" + " The `timestamp` of the item last received, to return log entries after that.\n" + " If not given, the query will return the top `ITEMS` from the most recent.\n" + "\n" + " facet_id:value_id1,value_id2,value_id3,...\n" + " Apply filters to the query, based on the facet IDs returned.\n" + " Each `facet_id` can be given once, but multiple `facet_ids` can be given.\n" + "\n" + "Filters can be combined. Each filter can be given only one time.\n" + , program_name + , SYSTEMD_JOURNAL_FUNCTION_NAME + , SYSTEMD_JOURNAL_FUNCTION_DESCRIPTION + , -SYSTEMD_JOURNAL_DEFAULT_QUERY_DURATION + , SYSTEMD_JOURNAL_DEFAULT_ITEMS_PER_QUERY + ); + pluginsd_function_result_end_to_stdout(); +} + +static const char *syslog_facility_to_name(int facility) { + switch (facility) { + case LOG_FAC(LOG_KERN): return "kern"; + case LOG_FAC(LOG_USER): return "user"; + case LOG_FAC(LOG_MAIL): return "mail"; + case LOG_FAC(LOG_DAEMON): return "daemon"; + case LOG_FAC(LOG_AUTH): return "auth"; + case LOG_FAC(LOG_SYSLOG): return "syslog"; + case LOG_FAC(LOG_LPR): return "lpr"; + case LOG_FAC(LOG_NEWS): return "news"; + case LOG_FAC(LOG_UUCP): return "uucp"; + case LOG_FAC(LOG_CRON): return "cron"; + case LOG_FAC(LOG_AUTHPRIV): return "authpriv"; + case LOG_FAC(LOG_FTP): return "ftp"; + case LOG_FAC(LOG_LOCAL0): return "local0"; + case LOG_FAC(LOG_LOCAL1): return "local1"; + case LOG_FAC(LOG_LOCAL2): return "local2"; + case LOG_FAC(LOG_LOCAL3): return "local3"; + case LOG_FAC(LOG_LOCAL4): return "local4"; + case LOG_FAC(LOG_LOCAL5): return "local5"; + case LOG_FAC(LOG_LOCAL6): return "local6"; + case LOG_FAC(LOG_LOCAL7): return "local7"; + default: return NULL; + } +} + +static const char *syslog_priority_to_name(int priority) { + switch (priority) { + case LOG_ALERT: return "alert"; + case LOG_CRIT: return "critical"; + case LOG_DEBUG: return "debug"; + case LOG_EMERG: return "panic"; + case LOG_ERR: return "error"; + case LOG_INFO: return "info"; + case LOG_NOTICE: return "notice"; + case LOG_WARNING: return "warning"; + default: return NULL; + } +} + +static char *uid_to_username(uid_t uid, char *buffer, size_t buffer_size) { + struct passwd pw, *result; + char tmp[1024 + 1]; + + if (getpwuid_r(uid, &pw, tmp, 1024, &result) != 0 || result == NULL) + return NULL; + + strncpy(buffer, pw.pw_name, buffer_size - 1); + buffer[buffer_size - 1] = '\0'; // Null-terminate just in case + return buffer; +} + +static char *gid_to_groupname(gid_t gid, char* buffer, size_t buffer_size) { + struct group grp, *result; + char tmp[1024 + 1]; + + if (getgrgid_r(gid, &grp, tmp, 1024, &result) != 0 || result == NULL) + return NULL; + + strncpy(buffer, grp.gr_name, buffer_size - 1); + buffer[buffer_size - 1] = '\0'; // Null-terminate just in case + return buffer; +} + +static void systemd_journal_transform_syslog_facility(FACETS *facets __maybe_unused, BUFFER *wb, void *data __maybe_unused) { + const char *v = buffer_tostring(wb); + if(*v && isdigit(*v)) { + int facility = str2i(buffer_tostring(wb)); + const char *name = syslog_facility_to_name(facility); + if (name) { + buffer_flush(wb); + buffer_json_add_array_item_string(wb, name); + } + } +} + +static void systemd_journal_transform_priority(FACETS *facets __maybe_unused, BUFFER *wb, void *data __maybe_unused) { + const char *v = buffer_tostring(wb); + if(*v && isdigit(*v)) { + int priority = str2i(buffer_tostring(wb)); + const char *name = syslog_priority_to_name(priority); + if (name) { + buffer_flush(wb); + buffer_json_add_array_item_string(wb, name); + } + } +} + +static void systemd_journal_transform_uid(FACETS *facets __maybe_unused, BUFFER *wb, void *data) { + DICTIONARY *cache = data; + const char *v = buffer_tostring(wb); + if(*v && isdigit(*v)) { + const char *sv = dictionary_get(cache, v); + if(!sv) { + char buf[1024 + 1]; + int uid = str2i(buffer_tostring(wb)); + const char *name = uid_to_username(uid, buf, 1024); + if (!name) + name = v; + + sv = dictionary_set(cache, v, (void *)name, strlen(name) + 1); + } + + buffer_flush(wb); + buffer_strcat(wb, sv); + } +} + +static void systemd_journal_transform_gid(FACETS *facets __maybe_unused, BUFFER *wb, void *data) { + DICTIONARY *cache = data; + const char *v = buffer_tostring(wb); + if(*v && isdigit(*v)) { + const char *sv = dictionary_get(cache, v); + if(!sv) { + char buf[1024 + 1]; + int gid = str2i(buffer_tostring(wb)); + const char *name = gid_to_groupname(gid, buf, 1024); + if (!name) + name = v; + + sv = dictionary_set(cache, v, (void *)name, strlen(name) + 1); + } + + buffer_flush(wb); + buffer_strcat(wb, sv); + } +} + +static void systemd_journal_dynamic_row_id(FACETS *facets __maybe_unused, BUFFER *wb, FACET_ROW_KEY_VALUE *rkv, FACET_ROW *row, void *data __maybe_unused) { + FACET_ROW_KEY_VALUE *syslog_identifier_rkv = dictionary_get(row->dict, "SYSLOG_IDENTIFIER"); + FACET_ROW_KEY_VALUE *pid_rkv = dictionary_get(row->dict, "_PID"); + + const char *identifier = syslog_identifier_rkv ? buffer_tostring(syslog_identifier_rkv->wb) : "UNKNOWN"; + const char *pid = pid_rkv ? buffer_tostring(pid_rkv->wb) : "UNKNOWN"; + + buffer_flush(rkv->wb); + buffer_sprintf(rkv->wb, "%s[%s]", identifier, pid); + + buffer_json_add_array_item_string(wb, buffer_tostring(rkv->wb)); +} + +static void function_systemd_journal(const char *transaction, char *function, char *line_buffer __maybe_unused, int line_max __maybe_unused, int timeout __maybe_unused) { + char *words[SYSTEMD_JOURNAL_MAX_PARAMS] = { NULL }; + size_t num_words = quoted_strings_splitter_pluginsd(function, words, SYSTEMD_JOURNAL_MAX_PARAMS); + + BUFFER *wb = buffer_create(0, NULL); + buffer_flush(wb); + buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_NEWLINE_ON_ARRAYS); + + FACETS *facets = facets_create(50, 0, FACETS_OPTION_ALL_KEYS_FTS, + SYSTEMD_ALWAYS_VISIBLE_KEYS, + SYSTEMD_KEYS_INCLUDED_IN_FACETS, + SYSTEMD_KEYS_EXCLUDED_FROM_FACETS); + + facets_accepted_param(facets, JOURNAL_PARAMETER_AFTER); + facets_accepted_param(facets, JOURNAL_PARAMETER_BEFORE); + facets_accepted_param(facets, JOURNAL_PARAMETER_ANCHOR); + facets_accepted_param(facets, JOURNAL_PARAMETER_LAST); + facets_accepted_param(facets, JOURNAL_PARAMETER_QUERY); + + // register the fields in the order you want them on the dashboard + + facets_register_dynamic_key(facets, "ND_JOURNAL_PROCESS", FACET_KEY_OPTION_NO_FACET|FACET_KEY_OPTION_VISIBLE|FACET_KEY_OPTION_FTS, + systemd_journal_dynamic_row_id, NULL); + + facets_register_key(facets, "MESSAGE", + FACET_KEY_OPTION_NO_FACET|FACET_KEY_OPTION_MAIN_TEXT|FACET_KEY_OPTION_VISIBLE|FACET_KEY_OPTION_FTS); + + facets_register_key_transformation(facets, "PRIORITY", FACET_KEY_OPTION_FACET|FACET_KEY_OPTION_FTS, + systemd_journal_transform_priority, NULL); + + facets_register_key_transformation(facets, "SYSLOG_FACILITY", FACET_KEY_OPTION_FACET|FACET_KEY_OPTION_FTS, + systemd_journal_transform_syslog_facility, NULL); + + facets_register_key(facets, "SYSLOG_IDENTIFIER", FACET_KEY_OPTION_FACET|FACET_KEY_OPTION_FTS); + facets_register_key(facets, "UNIT", FACET_KEY_OPTION_FACET|FACET_KEY_OPTION_FTS); + facets_register_key(facets, "USER_UNIT", FACET_KEY_OPTION_FACET|FACET_KEY_OPTION_FTS); + + facets_register_key_transformation(facets, "_UID", FACET_KEY_OPTION_FACET|FACET_KEY_OPTION_FTS, + systemd_journal_transform_uid, uids); + + facets_register_key_transformation(facets, "_GID", FACET_KEY_OPTION_FACET|FACET_KEY_OPTION_FTS, + systemd_journal_transform_gid, gids); + + time_t after_s = 0, before_s = 0; + usec_t anchor = 0; + size_t last = 0; + const char *query = NULL; + + buffer_json_member_add_object(wb, "request"); + buffer_json_member_add_object(wb, "filters"); + + for(int i = 1; i < SYSTEMD_JOURNAL_MAX_PARAMS ;i++) { + const char *keyword = get_word(words, num_words, i); + if(!keyword) break; + + if(strcmp(keyword, JOURNAL_PARAMETER_HELP) == 0) { + systemd_journal_function_help(transaction); + goto cleanup; + } + else if(strncmp(keyword, JOURNAL_PARAMETER_AFTER ":", strlen(JOURNAL_PARAMETER_AFTER ":")) == 0) { + after_s = str2l(&keyword[strlen(JOURNAL_PARAMETER_AFTER ":")]); + } + else if(strncmp(keyword, JOURNAL_PARAMETER_BEFORE ":", strlen(JOURNAL_PARAMETER_BEFORE ":")) == 0) { + before_s = str2l(&keyword[strlen(JOURNAL_PARAMETER_BEFORE ":")]); + } + else if(strncmp(keyword, JOURNAL_PARAMETER_ANCHOR ":", strlen(JOURNAL_PARAMETER_ANCHOR ":")) == 0) { + anchor = str2ull(&keyword[strlen(JOURNAL_PARAMETER_ANCHOR ":")], NULL); + } + else if(strncmp(keyword, JOURNAL_PARAMETER_LAST ":", strlen(JOURNAL_PARAMETER_LAST ":")) == 0) { + last = str2ul(&keyword[strlen(JOURNAL_PARAMETER_LAST ":")]); + } + else if(strncmp(keyword, JOURNAL_PARAMETER_QUERY ":", strlen(JOURNAL_PARAMETER_QUERY ":")) == 0) { + query= &keyword[strlen(JOURNAL_PARAMETER_QUERY ":")]; + } + else { + char *value = strchr(keyword, ':'); + if(value) { + *value++ = '\0'; + + buffer_json_member_add_array(wb, keyword); + + while(value) { + char *sep = strchr(value, ','); + if(sep) + *sep++ = '\0'; + + facets_register_facet_filter(facets, keyword, value, FACET_KEY_OPTION_REORDER); + buffer_json_add_array_item_string(wb, value); + + value = sep; + } + + buffer_json_array_close(wb); // keyword + } + } + } + + buffer_json_object_close(wb); // filters + + time_t expires = now_realtime_sec() + 1; + time_t now_s; + + if(!after_s && !before_s) { + now_s = now_realtime_sec(); + before_s = now_s; + after_s = before_s - SYSTEMD_JOURNAL_DEFAULT_QUERY_DURATION; + } + else + rrdr_relative_window_to_absolute(&after_s, &before_s, &now_s, false); + + if(after_s > before_s) { + time_t tmp = after_s; + after_s = before_s; + before_s = tmp; + } + + if(after_s == before_s) + after_s = before_s - SYSTEMD_JOURNAL_DEFAULT_QUERY_DURATION; + + if(!last) + last = SYSTEMD_JOURNAL_DEFAULT_ITEMS_PER_QUERY; + + buffer_json_member_add_time_t(wb, "after", after_s); + buffer_json_member_add_time_t(wb, "before", before_s); + buffer_json_member_add_uint64(wb, "anchor", anchor); + buffer_json_member_add_uint64(wb, "last", last); + buffer_json_member_add_string(wb, "query", query); + buffer_json_member_add_time_t(wb, "timeout", timeout); + buffer_json_object_close(wb); // request + + facets_set_items(facets, last); + facets_set_anchor(facets, anchor); + facets_set_query(facets, query); + int response = systemd_journal_query(wb, facets, after_s * USEC_PER_SEC, before_s * USEC_PER_SEC, + now_monotonic_usec() + (timeout - 1) * USEC_PER_SEC); + + if(response != HTTP_RESP_OK) { + pluginsd_function_json_error(transaction, response, "failed"); + goto cleanup; + } + + pluginsd_function_result_begin_to_stdout(transaction, HTTP_RESP_OK, "application/json", expires); + fwrite(buffer_tostring(wb), buffer_strlen(wb), 1, stdout); + + pluginsd_function_result_end_to_stdout(); + +cleanup: + facets_destroy(facets); + buffer_free(wb); +} + +static void *reader_main(void *arg __maybe_unused) { + char buffer[PLUGINSD_LINE_MAX + 1]; + + char *s = NULL; + while(!plugin_should_exit && (s = fgets(buffer, PLUGINSD_LINE_MAX, stdin))) { + + char *words[PLUGINSD_MAX_WORDS] = { NULL }; + size_t num_words = quoted_strings_splitter_pluginsd(buffer, words, PLUGINSD_MAX_WORDS); + + const char *keyword = get_word(words, num_words, 0); + + if(keyword && strcmp(keyword, PLUGINSD_KEYWORD_FUNCTION) == 0) { + char *transaction = get_word(words, num_words, 1); + char *timeout_s = get_word(words, num_words, 2); + char *function = get_word(words, num_words, 3); + + if(!transaction || !*transaction || !timeout_s || !*timeout_s || !function || !*function) { + netdata_log_error("Received incomplete %s (transaction = '%s', timeout = '%s', function = '%s'). Ignoring it.", + keyword, + transaction?transaction:"(unset)", + timeout_s?timeout_s:"(unset)", + function?function:"(unset)"); + } + else { + int timeout = str2i(timeout_s); + if(timeout <= 0) timeout = SYSTEMD_JOURNAL_DEFAULT_TIMEOUT; + + netdata_mutex_lock(&mutex); + + if(strncmp(function, SYSTEMD_JOURNAL_FUNCTION_NAME, strlen(SYSTEMD_JOURNAL_FUNCTION_NAME)) == 0) + function_systemd_journal(transaction, function, buffer, PLUGINSD_LINE_MAX + 1, timeout); + else + pluginsd_function_json_error(transaction, HTTP_RESP_NOT_FOUND, "No function with this name found in systemd-journal.plugin."); + + fflush(stdout); + netdata_mutex_unlock(&mutex); + } + } + else + netdata_log_error("Received unknown command: %s", keyword?keyword:"(unset)"); + } + + if(!s || feof(stdin) || ferror(stdin)) { + plugin_should_exit = true; + netdata_log_error("Received error on stdin."); + } + + exit(1); +} + +int main(int argc __maybe_unused, char **argv __maybe_unused) { + stderror = stderr; + clocks_init(); + + program_name = "systemd-journal.plugin"; + + // disable syslog + error_log_syslog = 0; + + // set errors flood protection to 100 logs per hour + error_log_errors_per_period = 100; + error_log_throttle_period = 3600; + + uids = dictionary_create(0); + gids = dictionary_create(0); + + // ------------------------------------------------------------------------ + // debug + + if(argc == 2 && strcmp(argv[1], "debug") == 0) { + char buf[] = "systemd-journal after:-86400 before:0 last:500"; + function_systemd_journal("123", buf, "", 0, 30); + exit(1); + } + + // ------------------------------------------------------------------------ + + netdata_thread_t reader_thread; + netdata_thread_create(&reader_thread, "SDJ_READER", NETDATA_THREAD_OPTION_DONT_LOG, reader_main, NULL); + + // ------------------------------------------------------------------------ + + time_t started_t = now_monotonic_sec(); + + size_t iteration; + usec_t step = 1000 * USEC_PER_MS; + bool tty = isatty(fileno(stderr)) == 1; + + netdata_mutex_lock(&mutex); + fprintf(stdout, PLUGINSD_KEYWORD_FUNCTION " \"%s\" %d \"%s\"\n", + SYSTEMD_JOURNAL_FUNCTION_NAME, SYSTEMD_JOURNAL_DEFAULT_TIMEOUT, SYSTEMD_JOURNAL_FUNCTION_DESCRIPTION); + + heartbeat_t hb; + heartbeat_init(&hb); + for(iteration = 0; 1 ; iteration++) { + netdata_mutex_unlock(&mutex); + heartbeat_next(&hb, step); + netdata_mutex_lock(&mutex); + + if(!tty) + fprintf(stdout, "\n"); + + fflush(stdout); + + time_t now = now_monotonic_sec(); + if(now - started_t > 86400) + break; + } + + dictionary_destroy(uids); + dictionary_destroy(gids); + + exit(0); +} diff --git a/configure.ac b/configure.ac index d2f047e61b..ac0d7bff6f 100644 --- a/configure.ac +++ b/configure.ac @@ -69,6 +69,12 @@ AC_ARG_ENABLE( , [enable_plugin_freeipmi="detect"] ) +AC_ARG_ENABLE( + [plugin-systemd-journal], + [AS_HELP_STRING([--enable-plugin-systemd-journal], [enable systemd-journal plugin @<:@default autodetect@:>@])], + , + [enable_plugin_systemd_journal="detect"] +) AC_ARG_ENABLE( [plugin-cups], [AS_HELP_STRING([--enable-plugin-cups], [enable cups plugin @<:@default autodetect@:>@])], @@ -1106,6 +1112,39 @@ AC_MSG_RESULT([${enable_plugin_freeipmi}]) AM_CONDITIONAL([ENABLE_PLUGIN_FREEIPMI], [test "${enable_plugin_freeipmi}" = "yes"]) +# ----------------------------------------------------------------------------- +# systemd-journal.plugin - systemd + +LIBS_BAK="${LIBS}" + +AC_CHECK_LIB([systemd], [sd_journal_open], [have_systemd_libs=yes], [have_systemd_libs=no]) +AC_CHECK_HEADERS([systemd/sd-journal.h], [have_systemd_journal_header=yes], [have_systemd_journal_header=no]) + +if test "${have_systemd_libs}" = "yes" -a "${have_systemd_journal_header}" = "yes"; then + have_systemd="yes" +else + have_systemd="no" +fi + +test "${enable_plugin_systemd_journal}" = "yes" -a "${have_systemd}" != "yes" && \ + AC_MSG_ERROR([systemd is required but not found. Try installing 'libsystemd-dev' or 'libsystemd-devel']) + +AC_MSG_CHECKING([if systemd-journal.plugin should be enabled]) +if test "${enable_plugin_systemd_journal}" != "no" -a "${have_systemd}" = "yes"; then + enable_plugin_systemd_journal="yes" + AC_DEFINE([HAVE_SYSTEMD], [1], [systemd usability]) + OPTIONAL_SYSTEMD_CFLAGS="-I/usr/include" + OPTIONAL_SYSTEMD_LIBS="-lsystemd" +else + enable_plugin_systemd_journal="no" +fi +AC_MSG_RESULT([${enable_plugin_systemd_journal}]) +AM_CONDITIONAL([ENABLE_PLUGIN_SYSTEMD_JOURNAL], [test "${enable_plugin_systemd_journal}" = "yes"]) + +AC_MSG_NOTICE([OPTIONAL_SYSTEMD_LIBS is set to: ${OPTIONAL_SYSTEMD_LIBS}]) + +LIBS="${LIBS_BAK}" + # ----------------------------------------------------------------------------- # cups.plugin - libcups @@ -1874,6 +1913,7 @@ AC_SUBST([OPTIONAL_GTEST_CFLAGS]) AC_SUBST([OPTIONAL_GTEST_LIBS]) AC_SUBST([OPTIONAL_ML_CFLAGS]) AC_SUBST([OPTIONAL_ML_LIBS]) +AC_SUBST(OPTIONAL_SYSTEMD_LIBS) # ----------------------------------------------------------------------------- # Check if cmocka is available - needed for unit testing @@ -1937,6 +1977,7 @@ AC_CONFIG_FILES([ collectors/tc.plugin/Makefile collectors/xenstat.plugin/Makefile collectors/perf.plugin/Makefile + collectors/systemd-journal.plugin/Makefile daemon/Makefile database/Makefile database/contexts/Makefile @@ -1968,6 +2009,7 @@ AC_CONFIG_FILES([ libnetdata/dictionary/Makefile libnetdata/ebpf/Makefile libnetdata/eval/Makefile + libnetdata/facets/Makefile libnetdata/july/Makefile libnetdata/locks/Makefile libnetdata/log/Makefile diff --git a/contrib/debian/rules b/contrib/debian/rules index 3fe4e63fac..d41b289b78 100755 --- a/contrib/debian/rules +++ b/contrib/debian/rules @@ -213,6 +213,9 @@ override_dh_fixperms: # local-listeners chmod 4750 $(TOP)/usr/libexec/netdata/plugins.d/local-listeners + # systemd-journal + # chmod 4750 $(TOP)/usr/libexec/netdata/plugins.d/systemd-journal.plugin + override_dh_installlogrotate: cp system/logrotate/netdata debian/netdata.logrotate dh_installlogrotate diff --git a/daemon/buildinfo.c b/daemon/buildinfo.c index 56cde84fc2..4bc1e72a4e 100644 --- a/daemon/buildinfo.c +++ b/daemon/buildinfo.c @@ -1469,7 +1469,7 @@ void print_build_info_json(void) { populate_directories(); BUFFER *b = buffer_create(0, NULL); - buffer_json_initialize(b, "\"", "\"", 0, true, false); + buffer_json_initialize(b, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_DEFAULT); build_info_to_json_object(b); diff --git a/daemon/main.c b/daemon/main.c index 9ed481cc5e..6ddf57aa17 100644 --- a/daemon/main.c +++ b/daemon/main.c @@ -1332,6 +1332,7 @@ int mrg_unittest(void); int julytest(void); int pluginsd_parser_unittest(void); void replication_initialize(void); +void bearer_tokens_init(void); int main(int argc, char **argv) { // initialize the system clocks diff --git a/database/contexts/api_v1.c b/database/contexts/api_v1.c index b4bcfe4ae3..bc7fee496d 100644 --- a/database/contexts/api_v1.c +++ b/database/contexts/api_v1.c @@ -148,7 +148,7 @@ static inline int rrdinstance_to_json_callback(const DICTIONARY_ITEM *item, void if(options & RRDCONTEXT_OPTION_SHOW_METRICS || t_parent->chart_dimensions) { wb_metrics = buffer_create(4096, &netdata_buffers_statistics.buffers_api); - buffer_json_initialize(wb_metrics, "\"", "\"", wb->json.depth + 2, false, false); + buffer_json_initialize(wb_metrics, "\"", "\"", wb->json.depth + 2, false, BUFFER_JSON_OPTIONS_DEFAULT); struct rrdcontext_to_json t_metrics = { .wb = wb_metrics, @@ -268,7 +268,7 @@ static inline int rrdcontext_to_json_callback(const DICTIONARY_ITEM *item, void || t_parent->chart_dimensions) { wb_instances = buffer_create(4096, &netdata_buffers_statistics.buffers_api); - buffer_json_initialize(wb_instances, "\"", "\"", wb->json.depth + 2, false, false); + buffer_json_initialize(wb_instances, "\"", "\"", wb->json.depth + 2, false, BUFFER_JSON_OPTIONS_DEFAULT); struct rrdcontext_to_json t_instances = { .wb = wb_instances, @@ -366,9 +366,9 @@ int rrdcontext_to_json(RRDHOST *host, BUFFER *wb, time_t after, time_t before, R RRDCONTEXT *rc = rrdcontext_acquired_value(rca); if(after != 0 && before != 0) - rrdr_relative_window_to_absolute(&after, &before, NULL); + rrdr_relative_window_to_absolute(&after, &before, NULL, false); - buffer_json_initialize(wb, "\"", "\"", 0, true, false); + buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_DEFAULT); struct rrdcontext_to_json t_contexts = { .wb = wb, .options = options|RRDCONTEXT_OPTION_SKIP_ID, @@ -403,9 +403,9 @@ int rrdcontexts_to_json(RRDHOST *host, BUFFER *wb, time_t after, time_t before, uuid_unparse(*host->node_id, node_uuid); if(after != 0 && before != 0) - rrdr_relative_window_to_absolute(&after, &before, NULL); + rrdr_relative_window_to_absolute(&after, &before, NULL, false); - buffer_json_initialize(wb, "\"", "\"", 0, true, false); + buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_DEFAULT); buffer_json_member_add_string(wb, "hostname", rrdhost_hostname(host)); buffer_json_member_add_string(wb, "machine_guid", host->machine_guid); buffer_json_member_add_string(wb, "node_id", node_uuid); diff --git a/database/contexts/api_v2.c b/database/contexts/api_v2.c index 88b55ad20a..08739160d9 100644 --- a/database/contexts/api_v2.c +++ b/database/contexts/api_v2.c @@ -1298,7 +1298,7 @@ int contexts_v2_alert_config_to_json(struct web_client *w, const char *config_ha buffer_flush(w->response.data); - buffer_json_initialize(w->response.data, "\"", "\"", 0, true, false); + buffer_json_initialize(w->response.data, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_DEFAULT); int added = sql_get_alert_configuration(configs, contexts_v2_alert_config_to_json_from_sql_alert_config_data, &data, false); buffer_json_finalize(w->response.data); @@ -1934,14 +1934,14 @@ int rrdcontext_to_json_v2(BUFFER *wb, struct api_v2_contexts_request *req, CONTE } if(req->after || req->before) { - ctl.window.relative = rrdr_relative_window_to_absolute(&ctl.window.after, &ctl.window.before, &ctl.now); + ctl.window.relative = rrdr_relative_window_to_absolute(&ctl.window.after, &ctl.window.before, &ctl.now, false); ctl.window.enabled = !(mode & CONTEXTS_V2_ALERT_TRANSITIONS); } else ctl.now = now_realtime_sec(); - buffer_json_initialize(wb, "\"", "\"", 0, - true, (req->options & CONTEXT_V2_OPTION_MINIFY) && !(req->options & CONTEXT_V2_OPTION_DEBUG)); + buffer_json_initialize(wb, "\"", "\"", 0, true, + ((req->options & CONTEXT_V2_OPTION_MINIFY) && !(req->options & CONTEXT_V2_OPTION_DEBUG)) ? BUFFER_JSON_OPTIONS_MINIFY : BUFFER_JSON_OPTIONS_DEFAULT); buffer_json_member_add_uint64(wb, "api", 2); diff --git a/database/contexts/query_target.c b/database/contexts/query_target.c index 16e1a73c97..829640b909 100644 --- a/database/contexts/query_target.c +++ b/database/contexts/query_target.c @@ -1052,7 +1052,8 @@ QUERY_TARGET *query_target_create(QUERY_TARGET_REQUEST *qtr) { if(query_target_has_percentage_of_group(qt)) qt->window.options &= ~RRDR_OPTION_PERCENTAGE; - qt->internal.relative = rrdr_relative_window_to_absolute(&qt->window.after, &qt->window.before, &qt->window.now); + qt->internal.relative = rrdr_relative_window_to_absolute(&qt->window.after, &qt->window.before, &qt->window.now, + unittest_running); // prepare our local variables - we need these across all these functions QUERY_TARGET_LOCALS qtl = { diff --git a/database/rrdfunctions.c b/database/rrdfunctions.c index cee1ac89ee..d32a4b8c91 100644 --- a/database/rrdfunctions.c +++ b/database/rrdfunctions.c @@ -270,7 +270,7 @@ static inline size_t sanitize_function_text(char *dst, const char *src, size_t d // we keep a dictionary per RRDSET with these functions // the dictionary is created on demand (only when a function is added to an RRDSET) -typedef enum { +typedef enum __attribute__((packed)) { RRD_FUNCTION_LOCAL = (1 << 0), RRD_FUNCTION_GLOBAL = (1 << 1), @@ -279,7 +279,7 @@ typedef enum { struct rrd_collector_function { bool sync; // when true, the function is called synchronously - uint8_t options; // RRD_FUNCTION_OPTIONS + RRD_FUNCTION_OPTIONS options; // RRD_FUNCTION_OPTIONS STRING *help; int timeout; // the default timeout of the function @@ -814,7 +814,7 @@ int rrdhost_function_streaming(BUFFER *wb, int timeout __maybe_unused, const cha buffer_flush(wb); wb->content_type = CT_APPLICATION_JSON; - buffer_json_initialize(wb, "\"", "\"", 0, true, false); + buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_DEFAULT); buffer_json_member_add_string(wb, "hostname", rrdhost_hostname(localhost)); buffer_json_member_add_uint64(wb, "status", HTTP_RESP_OK); @@ -858,8 +858,8 @@ int rrdhost_function_streaming(BUFFER *wb, int timeout __maybe_unused, const cha // retention buffer_json_add_array_item_string(wb, rrdhost_hostname(s.host)); // Node - buffer_json_add_array_item_uint64(wb, s.db.first_time_s * 1000); // dbFrom - buffer_json_add_array_item_uint64(wb, s.db.last_time_s * 1000); // dbTo + buffer_json_add_array_item_uint64(wb, s.db.first_time_s * MSEC_PER_SEC); // dbFrom + buffer_json_add_array_item_uint64(wb, s.db.last_time_s * MSEC_PER_SEC); // dbTo if(s.db.first_time_s && s.db.last_time_s && s.db.last_time_s > s.db.first_time_s) buffer_json_add_array_item_uint64(wb, s.db.last_time_s - s.db.first_time_s); // dbDuration @@ -877,7 +877,7 @@ int rrdhost_function_streaming(BUFFER *wb, int timeout __maybe_unused, const cha // collection if(s.ingest.since) { - buffer_json_add_array_item_uint64(wb, s.ingest.since * 1000); // InSince + buffer_json_add_array_item_uint64(wb, s.ingest.since * MSEC_PER_SEC); // InSince buffer_json_add_array_item_time_t(wb, s.now - s.ingest.since); // InAge } else { @@ -897,7 +897,7 @@ int rrdhost_function_streaming(BUFFER *wb, int timeout __maybe_unused, const cha // streaming if(s.stream.since) { - buffer_json_add_array_item_uint64(wb, s.stream.since * 1000); // OutSince + buffer_json_add_array_item_uint64(wb, s.stream.since * MSEC_PER_SEC); // OutSince buffer_json_add_array_item_time_t(wb, s.now - s.stream.since); // OutAge } else { @@ -990,19 +990,19 @@ int rrdhost_function_streaming(BUFFER *wb, int timeout __maybe_unused, const cha NULL); buffer_rrdf_table_add_field(wb, field_id++, "dbFrom", "DB Data Retention From", - RRDF_FIELD_TYPE_TIMESTAMP, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_DATETIME, + RRDF_FIELD_TYPE_TIMESTAMP, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_DATETIME_MS, 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, RRDF_FIELD_SUMMARY_MIN, RRDF_FIELD_FILTER_RANGE, RRDF_FIELD_OPTS_VISIBLE, NULL); buffer_rrdf_table_add_field(wb, field_id++, "dbTo", "DB Data Retention To", - RRDF_FIELD_TYPE_TIMESTAMP, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_DATETIME, + RRDF_FIELD_TYPE_TIMESTAMP, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_DATETIME_MS, 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, RRDF_FIELD_SUMMARY_MAX, RRDF_FIELD_FILTER_RANGE, RRDF_FIELD_OPTS_VISIBLE, NULL); buffer_rrdf_table_add_field(wb, field_id++, "dbDuration", "DB Data Retention Duration", - RRDF_FIELD_TYPE_DURATION, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_DURATION, + RRDF_FIELD_TYPE_DURATION, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_DURATION_S, 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, RRDF_FIELD_SUMMARY_MAX, RRDF_FIELD_FILTER_RANGE, RRDF_FIELD_OPTS_NONE, NULL); @@ -1049,13 +1049,13 @@ int rrdhost_function_streaming(BUFFER *wb, int timeout __maybe_unused, const cha // --- collection --- buffer_rrdf_table_add_field(wb, field_id++, "InSince", "Last Data Collection Status Change", - RRDF_FIELD_TYPE_TIMESTAMP, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_DATETIME, + RRDF_FIELD_TYPE_TIMESTAMP, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_DATETIME_MS, 0, NULL, NAN, RRDF_FIELD_SORT_DESCENDING, NULL, RRDF_FIELD_SUMMARY_MIN, RRDF_FIELD_FILTER_RANGE, RRDF_FIELD_OPTS_NONE, NULL); buffer_rrdf_table_add_field(wb, field_id++, "InAge", "Last Data Collection Online Status Change Age", - RRDF_FIELD_TYPE_DURATION, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_DURATION, + RRDF_FIELD_TYPE_DURATION, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_DURATION_S, 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, RRDF_FIELD_SUMMARY_MAX, RRDF_FIELD_FILTER_RANGE, RRDF_FIELD_OPTS_VISIBLE, NULL); @@ -1124,13 +1124,13 @@ int rrdhost_function_streaming(BUFFER *wb, int timeout __maybe_unused, const cha // --- streaming --- buffer_rrdf_table_add_field(wb, field_id++, "OutSince", "Last Streaming Status Change", - RRDF_FIELD_TYPE_TIMESTAMP, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_DATETIME, + RRDF_FIELD_TYPE_TIMESTAMP, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_DATETIME_MS, 0, NULL, NAN, RRDF_FIELD_SORT_DESCENDING, NULL, RRDF_FIELD_SUMMARY_MAX, RRDF_FIELD_FILTER_RANGE, RRDF_FIELD_OPTS_NONE, NULL); buffer_rrdf_table_add_field(wb, field_id++, "OutAge", "Last Streaming Status Change Age", - RRDF_FIELD_TYPE_DURATION, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_DURATION, + RRDF_FIELD_TYPE_DURATION, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_DURATION_S, 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, RRDF_FIELD_SUMMARY_MIN, RRDF_FIELD_FILTER_RANGE, RRDF_FIELD_OPTS_VISIBLE, NULL); @@ -1242,14 +1242,14 @@ int rrdhost_function_streaming(BUFFER *wb, int timeout __maybe_unused, const cha buffer_rrdf_table_add_field(wb, field_id++, "OutAttemptSince", "Last Outbound Connection Attempt Status Change Time", - RRDF_FIELD_TYPE_TIMESTAMP, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_DATETIME, + RRDF_FIELD_TYPE_TIMESTAMP, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_DATETIME_MS, 0, NULL, NAN, RRDF_FIELD_SORT_DESCENDING, NULL, RRDF_FIELD_SUMMARY_MAX, RRDF_FIELD_FILTER_RANGE, RRDF_FIELD_OPTS_NONE, NULL); buffer_rrdf_table_add_field(wb, field_id++, "OutAttemptAge", "Last Outbound Connection Attempt Status Change Age", - RRDF_FIELD_TYPE_DURATION, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_DURATION, + RRDF_FIELD_TYPE_DURATION, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_DURATION_S, 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, RRDF_FIELD_SUMMARY_MIN, RRDF_FIELD_FILTER_RANGE, RRDF_FIELD_OPTS_VISIBLE, NULL); diff --git a/libnetdata/Makefile.am b/libnetdata/Makefile.am index b81d620ba6..e85f4abe1b 100644 --- a/libnetdata/Makefile.am +++ b/libnetdata/Makefile.am @@ -14,6 +14,7 @@ SUBDIRS = \ dictionary \ ebpf \ eval \ + facets \ json \ july \ health \ diff --git a/libnetdata/buffer/buffer.c b/libnetdata/buffer/buffer.c index e775b95176..2d09bb1ff6 100644 --- a/libnetdata/buffer/buffer.c +++ b/libnetdata/buffer/buffer.c @@ -307,11 +307,11 @@ void buffer_increase(BUFFER *b, size_t free_size_required) { // ---------------------------------------------------------------------------- void buffer_json_initialize(BUFFER *wb, const char *key_quote, const char *value_quote, int depth, - bool add_anonymous_object, bool minify) { + bool add_anonymous_object, BUFFER_JSON_OPTIONS options) { strncpyz(wb->json.key_quote, key_quote, BUFFER_QUOTE_MAX_SIZE); strncpyz(wb->json.value_quote, value_quote, BUFFER_QUOTE_MAX_SIZE); - wb->json.minify = minify; + wb->json.options = options; wb->json.depth = (int8_t)(depth - 1); _buffer_json_depth_push(wb, BUFFER_JSON_OBJECT); @@ -339,7 +339,7 @@ void buffer_json_finalize(BUFFER *wb) { } } - if(!wb->json.minify) + if(!(wb->json.options & BUFFER_JSON_OPTIONS_MINIFY)) buffer_fast_strcat(wb, "\n", 1); } @@ -490,13 +490,13 @@ int buffer_unittest(void) { buffer_flush(wb); - buffer_json_initialize(wb, "\"", "\"", 0, true, false); + buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_DEFAULT); buffer_json_finalize(wb); errors += buffer_expect(wb, "{\n}\n"); buffer_flush(wb); - buffer_json_initialize(wb, "\"", "\"", 0, true, false); + buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_DEFAULT); buffer_json_member_add_string(wb, "hello", "world"); buffer_json_member_add_string(wb, "alpha", "this: \" is a double quote"); buffer_json_member_add_object(wb, "object1"); diff --git a/libnetdata/buffer/buffer.h b/libnetdata/buffer/buffer.h index d0078a521d..02134c8b26 100644 --- a/libnetdata/buffer/buffer.h +++ b/libnetdata/buffer/buffer.h @@ -68,6 +68,12 @@ typedef enum __attribute__ ((__packed__)) { CT_APPLICATION_ZIP, } HTTP_CONTENT_TYPE; +typedef enum __attribute__ ((__packed__)) { + BUFFER_JSON_OPTIONS_DEFAULT = 0, + BUFFER_JSON_OPTIONS_MINIFY = (1 << 0), + BUFFER_JSON_OPTIONS_NEWLINE_ON_ARRAYS = (1 << 1), +} BUFFER_JSON_OPTIONS; + typedef struct web_buffer { size_t size; // allocation size of buffer, in bytes size_t len; // current data length in buffer, in bytes @@ -82,7 +88,7 @@ typedef struct web_buffer { char key_quote[BUFFER_QUOTE_MAX_SIZE + 1]; char value_quote[BUFFER_QUOTE_MAX_SIZE + 1]; int8_t depth; - bool minify; + BUFFER_JSON_OPTIONS options; BUFFER_JSON_NODE stack[BUFFER_JSON_MAX_DEPTH]; } json; } BUFFER; @@ -148,7 +154,7 @@ static inline void buffer_need_bytes(BUFFER *buffer, size_t needed_free_size) { } void buffer_json_initialize(BUFFER *wb, const char *key_quote, const char *value_quote, int depth, - bool add_anonymous_object, bool minify); + bool add_anonymous_object, BUFFER_JSON_OPTIONS options); void buffer_json_finalize(BUFFER *wb); @@ -668,7 +674,8 @@ static inline void buffer_print_json_comma_newline_spacing(BUFFER *wb) { if(wb->json.stack[wb->json.depth].count) buffer_fast_strcat(wb, ",", 1); - if(wb->json.minify) + if((wb->json.options & BUFFER_JSON_OPTIONS_MINIFY) || + (wb->json.stack[wb->json.depth].type == BUFFER_JSON_ARRAY && !(wb->json.options & BUFFER_JSON_OPTIONS_NEWLINE_ON_ARRAYS))) return; buffer_fast_strcat(wb, "\n", 1); @@ -715,7 +722,7 @@ static inline void buffer_json_object_close(BUFFER *wb) { assert(wb->json.depth >= 0 && "BUFFER JSON: nothing is open to close it"); assert(wb->json.stack[wb->json.depth].type == BUFFER_JSON_OBJECT && "BUFFER JSON: an object is not open to close it"); #endif - if(!wb->json.minify) { + if(!(wb->json.options & BUFFER_JSON_OPTIONS_MINIFY)) { buffer_fast_strcat(wb, "\n", 1); buffer_print_spaces(wb, wb->json.depth); } @@ -801,48 +808,42 @@ static inline void buffer_json_add_array_item_array(BUFFER *wb) { } static inline void buffer_json_add_array_item_string(BUFFER *wb, const char *value) { - if(wb->json.stack[wb->json.depth].count) - buffer_fast_strcat(wb, ",", 1); + buffer_print_json_comma_newline_spacing(wb); buffer_json_add_string_value(wb, value); wb->json.stack[wb->json.depth].count++; } static inline void buffer_json_add_array_item_double(BUFFER *wb, NETDATA_DOUBLE value) { - if(wb->json.stack[wb->json.depth].count) - buffer_fast_strcat(wb, ",", 1); + buffer_print_json_comma_newline_spacing(wb); buffer_print_netdata_double(wb, value); wb->json.stack[wb->json.depth].count++; } static inline void buffer_json_add_array_item_int64(BUFFER *wb, int64_t value) { - if(wb->json.stack[wb->json.depth].count) - buffer_fast_strcat(wb, ",", 1); + buffer_print_json_comma_newline_spacing(wb); buffer_print_int64(wb, value); wb->json.stack[wb->json.depth].count++; } static inline void buffer_json_add_array_item_uint64(BUFFER *wb, uint64_t value) { - if(wb->json.stack[wb->json.depth].count) - buffer_fast_strcat(wb, ",", 1); + buffer_print_json_comma_newline_spacing(wb); buffer_print_uint64(wb, value); wb->json.stack[wb->json.depth].count++; } static inline void buffer_json_add_array_item_time_t(BUFFER *wb, time_t value) { - if(wb->json.stack[wb->json.depth].count) - buffer_fast_strcat(wb, ",", 1); + buffer_print_json_comma_newline_spacing(wb); buffer_print_int64(wb, value); wb->json.stack[wb->json.depth].count++; } static inline void buffer_json_add_array_item_time_ms(BUFFER *wb, time_t value) { - if(wb->json.stack[wb->json.depth].count) - buffer_fast_strcat(wb, ",", 1); + buffer_print_json_comma_newline_spacing(wb); buffer_print_int64(wb, value); buffer_fast_strcat(wb, "000", 3); @@ -850,8 +851,7 @@ static inline void buffer_json_add_array_item_time_ms(BUFFER *wb, time_t value) } static inline void buffer_json_add_array_item_time_t2ms(BUFFER *wb, time_t value) { - if(wb->json.stack[wb->json.depth].count) - buffer_fast_strcat(wb, ",", 1); + buffer_print_json_comma_newline_spacing(wb); buffer_print_int64(wb, value); buffer_fast_strcat(wb, "000", 3); @@ -859,8 +859,7 @@ static inline void buffer_json_add_array_item_time_t2ms(BUFFER *wb, time_t value } static inline void buffer_json_add_array_item_object(BUFFER *wb) { - if(wb->json.stack[wb->json.depth].count) - buffer_fast_strcat(wb, ",", 1); + buffer_print_json_comma_newline_spacing(wb); buffer_fast_strcat(wb, "{", 1); wb->json.stack[wb->json.depth].count++; @@ -919,6 +918,11 @@ static inline void buffer_json_array_close(BUFFER *wb) { assert(wb->json.depth >= 0 && "BUFFER JSON: nothing is open to close it"); assert(wb->json.stack[wb->json.depth].type == BUFFER_JSON_ARRAY && "BUFFER JSON: an array is not open to close it"); #endif + if(wb->json.options & BUFFER_JSON_OPTIONS_NEWLINE_ON_ARRAYS) { + buffer_fast_strcat(wb, "\n", 1); + buffer_print_spaces(wb, wb->json.depth); + } + buffer_fast_strcat(wb, "]", 1); _buffer_json_depth_pop(wb); } @@ -928,6 +932,8 @@ typedef enum __attribute__((packed)) { RRDF_FIELD_OPTS_UNIQUE_KEY = (1 << 0), // the field is the unique key of the row RRDF_FIELD_OPTS_VISIBLE = (1 << 1), // the field should be visible by default RRDF_FIELD_OPTS_STICKY = (1 << 2), // the field should be sticky + RRDF_FIELD_OPTS_FULL_WIDTH = (1 << 3), // the field should get full width + RRDF_FIELD_OPTS_WRAP = (1 << 4), // the field should get full width } RRDF_FIELD_OPTIONS; typedef enum __attribute__((packed)) { @@ -969,7 +975,8 @@ static inline const char *rrdf_field_type_to_string(RRDF_FIELD_TYPE type) { typedef enum __attribute__((packed)) { RRDF_FIELD_VISUAL_VALUE, // show the value, possibly applying a transformation RRDF_FIELD_VISUAL_BAR, // show the value and a bar, respecting the max field to fill the bar at 100% - RRDF_FIELD_VISUAL_PILL, // array of values (transformation is respected) + RRDF_FIELD_VISUAL_PILL, // + RRDF_FIELD_VISUAL_MARKDOC, // } RRDF_FIELD_VISUAL; static inline const char *rrdf_field_visual_to_string(RRDF_FIELD_VISUAL visual) { @@ -983,14 +990,18 @@ static inline const char *rrdf_field_visual_to_string(RRDF_FIELD_VISUAL visual) case RRDF_FIELD_VISUAL_PILL: return "pill"; + + case RRDF_FIELD_VISUAL_MARKDOC: + return "markdoc"; } } typedef enum __attribute__((packed)) { RRDF_FIELD_TRANSFORM_NONE, // show the value as-is - RRDF_FIELD_TRANSFORM_NUMBER, // show the value repsecting the decimal_points - RRDF_FIELD_TRANSFORM_DURATION, // transform as duration in second to a human readable duration - RRDF_FIELD_TRANSFORM_DATETIME, // UNIX epoch timestamp in ms + RRDF_FIELD_TRANSFORM_NUMBER, // show the value respecting the decimal_points + RRDF_FIELD_TRANSFORM_DURATION_S, // transform as duration in second to a human-readable duration + RRDF_FIELD_TRANSFORM_DATETIME_MS, // UNIX epoch timestamp in ms + RRDF_FIELD_TRANSFORM_DATETIME_USEC, // UNIX epoch timestamp in usec } RRDF_FIELD_TRANSFORM; static inline const char *rrdf_field_transform_to_string(RRDF_FIELD_TRANSFORM transform) { @@ -1002,11 +1013,14 @@ static inline const char *rrdf_field_transform_to_string(RRDF_FIELD_TRANSFORM tr case RRDF_FIELD_TRANSFORM_NUMBER: return "number"; - case RRDF_FIELD_TRANSFORM_DURATION: + case RRDF_FIELD_TRANSFORM_DURATION_S: return "duration"; - case RRDF_FIELD_TRANSFORM_DATETIME: + case RRDF_FIELD_TRANSFORM_DATETIME_MS: return "datetime"; + + case RRDF_FIELD_TRANSFORM_DATETIME_USEC: + return "datetime_usec"; } } @@ -1064,18 +1078,26 @@ static inline const char *rrdf_field_summary_to_string(RRDF_FIELD_SUMMARY summar } typedef enum __attribute__((packed)) { + RRDF_FIELD_FILTER_NONE, RRDF_FIELD_FILTER_RANGE, RRDF_FIELD_FILTER_MULTISELECT, + RRDF_FIELD_FILTER_FACET, } RRDF_FIELD_FILTER; static inline const char *rrdf_field_filter_to_string(RRDF_FIELD_FILTER filter) { switch(filter) { - default: case RRDF_FIELD_FILTER_RANGE: return "range"; case RRDF_FIELD_FILTER_MULTISELECT: return "multiselect"; + + case RRDF_FIELD_FILTER_FACET: + return "facet"; + + default: + case RRDF_FIELD_FILTER_NONE: + return "none"; } } @@ -1114,6 +1136,9 @@ buffer_rrdf_table_add_field(BUFFER *wb, size_t field_id, const char *key, const buffer_json_member_add_boolean(wb, "sticky", options & RRDF_FIELD_OPTS_STICKY); buffer_json_member_add_string(wb, "summary", rrdf_field_summary_to_string(summary)); buffer_json_member_add_string(wb, "filter", rrdf_field_filter_to_string(filter)); + + buffer_json_member_add_boolean(wb, "full_width", options & RRDF_FIELD_OPTS_FULL_WIDTH); + buffer_json_member_add_boolean(wb, "wrap", options & RRDF_FIELD_OPTS_WRAP); } buffer_json_object_close(wb); } diff --git a/libnetdata/facets/Makefile.am b/libnetdata/facets/Makefile.am new file mode 100644 index 0000000000..161784b8f6 --- /dev/null +++ b/libnetdata/facets/Makefile.am @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: GPL-3.0-or-later + +AUTOMAKE_OPTIONS = subdir-objects +MAINTAINERCLEANFILES = $(srcdir)/Makefile.in + +dist_noinst_DATA = \ + README.md \ + $(NULL) diff --git a/libnetdata/facets/README.md b/libnetdata/facets/README.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libnetdata/facets/facets.c b/libnetdata/facets/facets.c new file mode 100644 index 0000000000..5a0ea30f76 --- /dev/null +++ b/libnetdata/facets/facets.c @@ -0,0 +1,827 @@ +#include "facets.h" + +#define FACET_VALUE_UNSET "-" +#define HISTOGRAM_COLUMNS 60 + +static void facets_row_free(FACETS *facets __maybe_unused, FACET_ROW *row); + +// ---------------------------------------------------------------------------- + +time_t calculate_bar_width(time_t before, time_t after) { + // Array of valid durations in seconds + static time_t valid_durations[] = { + 1, + 15, + 30, + 1 * 60, 2 * 60, 3 * 60, 5 * 60, 10 * 60, 15 * 60, 30 * 60, // minutes + 1 * 3600, 2 * 3600, 6 * 3600, 8 * 3600, 12 * 3600, // hours + 1 * 86400, 2 * 86400, 3 * 86400, 5 * 86400, 7 * 86400, 14 * 86400, // days + 1 * (30*86400) // months + }; + static int array_size = sizeof(valid_durations) / sizeof(valid_durations[0]); + + time_t duration = before - after; + time_t bar_width = 1; + + for (int i = array_size - 1; i >= 0; --i) { + if (duration / valid_durations[i] >= HISTOGRAM_COLUMNS) { + bar_width = valid_durations[i]; + break; + } + } + + return bar_width; +} + +// ---------------------------------------------------------------------------- + +static inline void uint32_to_char(uint32_t num, char *out) { + static char id_encoding_characters[64 + 1] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ.abcdefghijklmnopqrstuvwxyz_0123456789"; + + int i; + for(i = 5; i >= 0; --i) { + out[i] = id_encoding_characters[num & 63]; + num >>= 6; + } + out[6] = '\0'; +} + +inline void facets_string_hash(const char *src, char *out) { + uint32_t hash1 = fnv1a_hash32(src); + uint32_t hash2 = djb2_hash32(src); + uint32_t hash3 = larson_hash32(src); + + uint32_to_char(hash1, out); + uint32_to_char(hash2, &out[6]); + uint32_to_char(hash3, &out[12]); + + out[18] = '\0'; +} + +// ---------------------------------------------------------------------------- + +typedef struct facet_value { + const char *name; + + bool selected; + + uint32_t rows_matching_facet_value; + uint32_t final_facet_value_counter; +} FACET_VALUE; + +struct facet_key { + const char *name; + + DICTIONARY *values; + + FACET_KEY_OPTIONS options; + + bool default_selected_for_values; // the default "selected" for all values in the dictionary + + // members about the current row + uint32_t key_found_in_row; + uint32_t key_values_selected_in_row; + BUFFER *current_value; + + uint32_t order; + + struct { + facet_dynamic_row_t cb; + void *data; + } dynamic; + + struct { + facets_key_transformer_t cb; + void *data; + + } transform; +}; + +struct facets { + SIMPLE_PATTERN *visible_keys; + SIMPLE_PATTERN *excluded_keys; + SIMPLE_PATTERN *included_keys; + + FACETS_OPTIONS options; + usec_t anchor; + + SIMPLE_PATTERN *query; // the full text search pattern + size_t keys_filtered_by_query; // the number of fields we do full text search (constant) + size_t keys_matched_by_query; // the number of fields matched the full text search (per row) + + DICTIONARY *accepted_params; + + DICTIONARY *keys; + FACET_ROW *base; // double linked list of the selected facets rows + + uint32_t items_to_return; + uint32_t max_items_to_return; + uint32_t order; + + struct { + FACET_ROW *last_added; + + size_t evaluated; + size_t matched; + + size_t first; + size_t forwards; + size_t backwards; + size_t skips_before; + size_t skips_after; + size_t prepends; + size_t appends; + size_t shifts; + } operations; +}; + +// ---------------------------------------------------------------------------- + +static inline void facet_value_is_used(FACET_KEY *k, FACET_VALUE *v) { + if(!k->key_found_in_row) + v->rows_matching_facet_value++; + + k->key_found_in_row++; + + if(v->selected) + k->key_values_selected_in_row++; +} + +static inline bool facets_key_is_facet(FACETS *facets, FACET_KEY *k) { + bool included = true, excluded = false; + + if(k->options & (FACET_KEY_OPTION_FACET | FACET_KEY_OPTION_NO_FACET)) { + if(k->options & FACET_KEY_OPTION_FACET) { + included = true; + excluded = false; + } + else if(k->options & FACET_KEY_OPTION_NO_FACET) { + included = false; + excluded = true; + } + } + else { + if (facets->included_keys) { + if (!simple_pattern_matches(facets->included_keys, k->name)) + included = false; + } + + if (facets->excluded_keys) { + if (simple_pattern_matches(facets->excluded_keys, k->name)) + excluded = true; + } + } + + if(included && !excluded) { + k->options |= FACET_KEY_OPTION_FACET; + k->options &= ~FACET_KEY_OPTION_NO_FACET; + return true; + } + + k->options |= FACET_KEY_OPTION_NO_FACET; + k->options &= ~FACET_KEY_OPTION_FACET; + return false; +} + +// ---------------------------------------------------------------------------- +// FACET_VALUE dictionary hooks + +static void facet_value_insert_callback(const DICTIONARY_ITEM *item __maybe_unused, void *value, void *data) { + FACET_VALUE *v = value; + FACET_KEY *k = data; + + if(v->name) { + // an actual value, not a filter + v->name = strdupz(v->name); + facet_value_is_used(k, v); + } + + if(!v->selected) + v->selected = k->default_selected_for_values; +} + +static bool facet_value_conflict_callback(const DICTIONARY_ITEM *item __maybe_unused, void *old_value, void *new_value, void *data) { + FACET_VALUE *v = old_value; + FACET_VALUE *nv = new_value; + FACET_KEY *k = data; + + if(!v->name && nv->name) + // an actual value, not a filter + v->name = strdupz(nv->name); + + if(v->name) + facet_value_is_used(k, v); + + return false; +} + +static void facet_value_delete_callback(const DICTIONARY_ITEM *item __maybe_unused, void *value, void *data __maybe_unused) { + FACET_VALUE *v = value; + freez((char *)v->name); +} + +// ---------------------------------------------------------------------------- +// FACET_KEY dictionary hooks + +static inline void facet_key_late_init(FACETS *facets, FACET_KEY *k) { + if(k->values) + return; + + if(facets_key_is_facet(facets, k)) { + k->values = dictionary_create_advanced( + DICT_OPTION_SINGLE_THREADED | DICT_OPTION_DONT_OVERWRITE_VALUE | DICT_OPTION_FIXED_SIZE, + NULL, sizeof(FACET_VALUE)); + dictionary_register_insert_callback(k->values, facet_value_insert_callback, k); + dictionary_register_conflict_callback(k->values, facet_value_conflict_callback, k); + dictionary_register_delete_callback(k->values, facet_value_delete_callback, k); + } +} + +static void facet_key_insert_callback(const DICTIONARY_ITEM *item __maybe_unused, void *value, void *data) { + FACET_KEY *k = value; + FACETS *facets = data; + + if(!(k->options & FACET_KEY_OPTION_REORDER)) + k->order = facets->order++; + + if((k->options & FACET_KEY_OPTION_FTS) || (facets->options & FACETS_OPTION_ALL_KEYS_FTS)) + facets->keys_filtered_by_query++; + + if(k->name) { + // an actual value, not a filter + k->name = strdupz(k->name); + facet_key_late_init(facets, k); + } + + k->current_value = buffer_create(0, NULL); +} + +static bool facet_key_conflict_callback(const DICTIONARY_ITEM *item __maybe_unused, void *old_value, void *new_value, void *data) { + FACET_KEY *k = old_value; + FACET_KEY *nk = new_value; + FACETS *facets = data; + + if(!k->name && nk->name) { + // an actual value, not a filter + k->name = strdupz(nk->name); + facet_key_late_init(facets, k); + } + + if(k->options & FACET_KEY_OPTION_REORDER) { + k->order = facets->order++; + k->options &= ~FACET_KEY_OPTION_REORDER; + } + + return false; +} + +static void facet_key_delete_callback(const DICTIONARY_ITEM *item __maybe_unused, void *value, void *data __maybe_unused) { + FACET_KEY *k = value; + dictionary_destroy(k->values); + buffer_free(k->current_value); + freez((char *)k->name); +} + +// ---------------------------------------------------------------------------- + +FACETS *facets_create(uint32_t items_to_return, usec_t anchor, FACETS_OPTIONS options, const char *visible_keys, const char *facet_keys, const char *non_facet_keys) { + FACETS *facets = callocz(1, sizeof(FACETS)); + facets->options = options; + facets->keys = dictionary_create_advanced(DICT_OPTION_SINGLE_THREADED|DICT_OPTION_DONT_OVERWRITE_VALUE|DICT_OPTION_FIXED_SIZE, NULL, sizeof(FACET_KEY)); + dictionary_register_insert_callback(facets->keys, facet_key_insert_callback, facets); + dictionary_register_conflict_callback(facets->keys, facet_key_conflict_callback, facets); + dictionary_register_delete_callback(facets->keys, facet_key_delete_callback, facets); + + if(facet_keys && *facet_keys) + facets->included_keys = simple_pattern_create(facet_keys, "|", SIMPLE_PATTERN_EXACT, true); + + if(non_facet_keys && *non_facet_keys) + facets->excluded_keys = simple_pattern_create(non_facet_keys, "|", SIMPLE_PATTERN_EXACT, true); + + if(visible_keys && *visible_keys) + facets->visible_keys = simple_pattern_create(visible_keys, "|", SIMPLE_PATTERN_EXACT, true); + + facets->max_items_to_return = items_to_return; + facets->anchor = anchor; + facets->order = 1; + + return facets; +} + +void facets_destroy(FACETS *facets) { + dictionary_destroy(facets->accepted_params); + dictionary_destroy(facets->keys); + simple_pattern_free(facets->visible_keys); + simple_pattern_free(facets->included_keys); + simple_pattern_free(facets->excluded_keys); + + while(facets->base) { + FACET_ROW *r = facets->base; + DOUBLE_LINKED_LIST_REMOVE_ITEM_UNSAFE(facets->base, r, prev, next); + + facets_row_free(facets, r); + } + + freez(facets); +} + +void facets_accepted_param(FACETS *facets, const char *param) { + if(!facets->accepted_params) + facets->accepted_params = dictionary_create(DICT_OPTION_SINGLE_THREADED|DICT_OPTION_DONT_OVERWRITE_VALUE); + + dictionary_set(facets->accepted_params, param, NULL, 0); +} + +inline FACET_KEY *facets_register_key(FACETS *facets, const char *key, FACET_KEY_OPTIONS options) { + FACET_KEY tk = { + .name = key, + .options = options, + .default_selected_for_values = true, + }; + char hash[FACET_STRING_HASH_SIZE]; + facets_string_hash(tk.name, hash); + return dictionary_set(facets->keys, hash, &tk, sizeof(tk)); +} + +inline FACET_KEY *facets_register_key_transformation(FACETS *facets, const char *key, FACET_KEY_OPTIONS options, facets_key_transformer_t cb, void *data) { + FACET_KEY *k = facets_register_key(facets, key, options); + k->transform.cb = cb; + k->transform.data = data; + return k; +} + +inline FACET_KEY *facets_register_dynamic_key(FACETS *facets, const char *key, FACET_KEY_OPTIONS options, facet_dynamic_row_t cb, void *data) { + FACET_KEY *k = facets_register_key(facets, key, options); + k->dynamic.cb = cb; + k->dynamic.data = data; + return k; +} + +void facets_set_query(FACETS *facets, const char *query) { + if(!query) + return; + + facets->query = simple_pattern_create(query, " \t", SIMPLE_PATTERN_SUBSTRING, false); +} + +void facets_set_items(FACETS *facets, uint32_t items) { + facets->max_items_to_return = items; +} + +void facets_set_anchor(FACETS *facets, usec_t anchor) { + facets->anchor = anchor; +} + +void facets_register_facet_filter(FACETS *facets, const char *key_id, char *value_ids, FACET_KEY_OPTIONS options) { + FACET_KEY tk = { + .options = options, + }; + FACET_KEY *k = dictionary_set(facets->keys, key_id, &tk, sizeof(tk)); + + k->default_selected_for_values = false; + k->options |= FACET_KEY_OPTION_FACET; + k->options &= ~FACET_KEY_OPTION_NO_FACET; + facet_key_late_init(facets, k); + + FACET_VALUE tv = { + .selected = true, + }; + dictionary_set(k->values, value_ids, &tv, sizeof(tv)); +} + +// ---------------------------------------------------------------------------- + +static inline void facets_check_value(FACETS *facets __maybe_unused, FACET_KEY *k) { + if(k->transform.cb) + k->transform.cb(facets, k->current_value, k->transform.data); + + if(buffer_strlen(k->current_value) == 0) + buffer_strcat(k->current_value, FACET_VALUE_UNSET); + +// bool found = false; +// if(strstr(buffer_tostring(k->current_value), "fprintd") != NULL) +// found = true; + + if(facets->query && ((k->options & FACET_KEY_OPTION_FTS) || facets->options & FACETS_OPTION_ALL_KEYS_FTS)) { + if(simple_pattern_matches(facets->query, buffer_tostring(k->current_value))) + facets->keys_matched_by_query++; + } + + if(k->values) { + FACET_VALUE tk = { + .name = buffer_tostring(k->current_value), + }; + char hash[FACET_STRING_HASH_SIZE]; + facets_string_hash(tk.name, hash); + dictionary_set(k->values, hash, &tk, sizeof(tk)); + } + else { + k->key_found_in_row++; + k->key_values_selected_in_row++; + } +} + +void facets_add_key_value(FACETS *facets, const char *key, const char *value) { + FACET_KEY *k = facets_register_key(facets, key, 0); + buffer_flush(k->current_value); + buffer_strcat(k->current_value, value); + + facets_check_value(facets, k); +} + +void facets_add_key_value_length(FACETS *facets, const char *key, const char *value, size_t value_len) { + FACET_KEY *k = facets_register_key(facets, key, 0); + buffer_flush(k->current_value); + buffer_strncat(k->current_value, value, value_len); + + facets_check_value(facets, k); +} + +// ---------------------------------------------------------------------------- +// FACET_ROW dictionary hooks + +static void facet_row_key_value_insert_callback(const DICTIONARY_ITEM *item __maybe_unused, void *value, void *data) { + FACET_ROW_KEY_VALUE *rkv = value; + FACET_ROW *row = data; (void)row; + + rkv->wb = buffer_create(0, NULL); + buffer_strcat(rkv->wb, rkv->tmp && *rkv->tmp ? rkv->tmp : FACET_VALUE_UNSET); +} + +static bool facet_row_key_value_conflict_callback(const DICTIONARY_ITEM *item __maybe_unused, void *old_value, void *new_value, void *data) { + FACET_ROW_KEY_VALUE *rkv = old_value; + FACET_ROW_KEY_VALUE *n_rkv = new_value; + FACET_ROW *row = data; (void)row; + + buffer_flush(rkv->wb); + buffer_strcat(rkv->wb, n_rkv->tmp && *n_rkv->tmp ? n_rkv->tmp : FACET_VALUE_UNSET); + + return false; +} + +static void facet_row_key_value_delete_callback(const DICTIONARY_ITEM *item __maybe_unused, void *value, void *data) { + FACET_ROW_KEY_VALUE *rkv = value; + FACET_ROW *row = data; (void)row; + + buffer_free(rkv->wb); +} + +// ---------------------------------------------------------------------------- +// FACET_ROW management + +static void facets_row_free(FACETS *facets __maybe_unused, FACET_ROW *row) { + dictionary_destroy(row->dict); + freez(row); +} + +static FACET_ROW *facets_row_create(FACETS *facets, usec_t usec, FACET_ROW *into) { + FACET_ROW *row; + + if(into) + row = into; + else { + row = callocz(1, sizeof(FACET_ROW)); + row->dict = dictionary_create_advanced(DICT_OPTION_SINGLE_THREADED|DICT_OPTION_DONT_OVERWRITE_VALUE|DICT_OPTION_FIXED_SIZE, NULL, sizeof(FACET_ROW_KEY_VALUE)); + dictionary_register_insert_callback(row->dict, facet_row_key_value_insert_callback, row); + dictionary_register_conflict_callback(row->dict, facet_row_key_value_conflict_callback, row); + dictionary_register_delete_callback(row->dict, facet_row_key_value_delete_callback, row); + } + + row->usec = usec; + + FACET_KEY *k; + dfe_start_read(facets->keys, k) { + FACET_ROW_KEY_VALUE t = { + .tmp = buffer_strlen(k->current_value) ? buffer_tostring(k->current_value) : FACET_VALUE_UNSET, + .wb = NULL, + }; + dictionary_set(row->dict, k->name, &t, sizeof(t)); + } + dfe_done(k); + + return row; +} + +// ---------------------------------------------------------------------------- + +static void facets_row_keep(FACETS *facets, usec_t usec) { + facets->operations.matched++; + + if(usec < facets->anchor) { + facets->operations.skips_before++; + return; + } + + if(unlikely(!facets->base)) { + facets->operations.last_added = facets_row_create(facets, usec, NULL); + DOUBLE_LINKED_LIST_APPEND_ITEM_UNSAFE(facets->base, facets->operations.last_added, prev, next); + facets->items_to_return++; + facets->operations.first++; + return; + } + + if(likely(usec > facets->base->prev->usec)) + facets->operations.last_added = facets->base->prev; + + FACET_ROW *last = facets->operations.last_added; + while(last->prev != facets->base->prev && usec > last->prev->usec) { + last = last->prev; + facets->operations.backwards++; + } + + while(last->next && usec < last->next->usec) { + last = last->next; + facets->operations.forwards++; + } + + if(facets->items_to_return >= facets->max_items_to_return) { + if(last == facets->base->prev && usec < last->usec) { + facets->operations.skips_after++; + return; + } + } + + facets->items_to_return++; + + if(usec > last->usec) { + if(facets->items_to_return > facets->max_items_to_return) { + facets->items_to_return--; + facets->operations.shifts++; + facets->operations.last_added = facets->base->prev; + DOUBLE_LINKED_LIST_REMOVE_ITEM_UNSAFE(facets->base, facets->operations.last_added, prev, next); + facets->operations.last_added = facets_row_create(facets, usec, facets->operations.last_added); + } + DOUBLE_LINKED_LIST_PREPEND_ITEM_UNSAFE(facets->base, facets->operations.last_added, prev, next); + facets->operations.prepends++; + } + else { + facets->operations.last_added = facets_row_create(facets, usec, NULL); + DOUBLE_LINKED_LIST_APPEND_ITEM_UNSAFE(facets->base, facets->operations.last_added, prev, next); + facets->operations.appends++; + } + + while(facets->items_to_return > facets->max_items_to_return) { + // we have to remove something + + FACET_ROW *tmp = facets->base->prev; + DOUBLE_LINKED_LIST_REMOVE_ITEM_UNSAFE(facets->base, tmp, prev, next); + facets->items_to_return--; + + if(unlikely(facets->operations.last_added == tmp)) + facets->operations.last_added = facets->base->prev; + + facets_row_free(facets, tmp); + facets->operations.shifts++; + } +} + +void facets_rows_begin(FACETS *facets) { + FACET_KEY *k; + dfe_start_read(facets->keys, k) { + k->key_found_in_row = 0; + k->key_values_selected_in_row = 0; + buffer_flush(k->current_value); + } + dfe_done(k); + + facets->keys_matched_by_query = 0; +} + +void facets_row_finished(FACETS *facets, usec_t usec) { + if(facets->query && facets->keys_filtered_by_query && !facets->keys_matched_by_query) + goto cleanup; + + facets->operations.evaluated++; + + uint32_t total_keys = 0; + uint32_t selected_by = 0; + + FACET_KEY *k; + dfe_start_read(facets->keys, k) { + if(!k->key_found_in_row) { + internal_fatal(buffer_strlen(k->current_value), "key is not found in row but it has a current value"); + // put the FACET_VALUE_UNSET value into it + facets_check_value(facets, k); + } + + internal_fatal(!k->key_found_in_row, "all keys should be found in the row at this point"); + internal_fatal(k->key_found_in_row != 1, "all keys should be matched exactly once at this point"); + internal_fatal(k->key_values_selected_in_row > 1, "key values are selected in row more than once"); + + k->key_found_in_row = 1; + + total_keys += k->key_found_in_row; + selected_by += (k->key_values_selected_in_row) ? 1 : 0; + } + dfe_done(k); + + if(selected_by >= total_keys - 1) { + uint32_t found = 0; + + dfe_start_read(facets->keys, k){ + uint32_t counted_by = selected_by; + + if (counted_by != total_keys && !k->key_values_selected_in_row) + counted_by++; + + if(counted_by == total_keys) { + if(k->values) { + char hash[FACET_STRING_HASH_SIZE]; + facets_string_hash(buffer_tostring(k->current_value), hash); + FACET_VALUE *v = dictionary_get(k->values, hash); + v->final_facet_value_counter++; + } + + found++; + } + } + dfe_done(k); + + internal_fatal(!found, "We should find at least one facet to count this row"); + (void)found; + } + + if(selected_by == total_keys) + facets_row_keep(facets, usec); + +cleanup: + facets_rows_begin(facets); +} + +// ---------------------------------------------------------------------------- +// output + +void facets_report(FACETS *facets, BUFFER *wb) { + buffer_json_member_add_boolean(wb, "show_ids", false); + buffer_json_member_add_boolean(wb, "has_history", true); + + buffer_json_member_add_object(wb, "pagination"); + buffer_json_member_add_boolean(wb, "enabled", true); + buffer_json_member_add_string(wb, "key", "anchor"); + buffer_json_member_add_string(wb, "column", "timestamp"); + buffer_json_object_close(wb); + + buffer_json_member_add_array(wb, "accepted_params"); + { + if(facets->accepted_params) { + void *t; + dfe_start_read(facets->accepted_params, t) { + buffer_json_add_array_item_string(wb, t_dfe.name); + } + dfe_done(t); + } + + FACET_KEY *k; + dfe_start_read(facets->keys, k) { + if(!k->values) + continue; + + buffer_json_add_array_item_string(wb, k_dfe.name); + } + dfe_done(k); + } + buffer_json_array_close(wb); // accepted_params + + buffer_json_member_add_array(wb, "facets"); + { + FACET_KEY *k; + dfe_start_read(facets->keys, k) { + if(!k->values) + continue; + + buffer_json_add_array_item_object(wb); // key + { + buffer_json_member_add_string(wb, "id", k_dfe.name); + buffer_json_member_add_string(wb, "name", k->name); + + if(!k->order) + k->order = facets->order++; + + buffer_json_member_add_uint64(wb, "order", k->order); + buffer_json_member_add_array(wb, "options"); + { + FACET_VALUE *v; + dfe_start_read(k->values, v) { + buffer_json_add_array_item_object(wb); + { + buffer_json_member_add_string(wb, "id", v_dfe.name); + buffer_json_member_add_string(wb, "name", v->name); + buffer_json_member_add_uint64(wb, "count", v->final_facet_value_counter); + } + buffer_json_object_close(wb); + } + dfe_done(v); + } + buffer_json_array_close(wb); // options + } + buffer_json_object_close(wb); // key + } + dfe_done(k); + } + buffer_json_array_close(wb); // facets + + buffer_json_member_add_object(wb, "columns"); + { + size_t field_id = 0; + buffer_rrdf_table_add_field( + wb, field_id++, + "timestamp", "Timestamp", + RRDF_FIELD_TYPE_TIMESTAMP, + RRDF_FIELD_VISUAL_VALUE, + RRDF_FIELD_TRANSFORM_DATETIME_USEC, 0, NULL, NAN, + RRDF_FIELD_SORT_DESCENDING, + NULL, + RRDF_FIELD_SUMMARY_COUNT, + RRDF_FIELD_FILTER_RANGE, + RRDF_FIELD_OPTS_VISIBLE | RRDF_FIELD_OPTS_UNIQUE_KEY, + NULL); + + FACET_KEY *k; + dfe_start_read(facets->keys, k) { + RRDF_FIELD_OPTIONS options = RRDF_FIELD_OPTS_NONE; + bool visible = k->options & (FACET_KEY_OPTION_VISIBLE|FACET_KEY_OPTION_STICKY); + + if((facets->options & FACETS_OPTION_ALL_FACETS_VISIBLE && k->values)) + visible = true; + + if(!visible) + visible = simple_pattern_matches(facets->visible_keys, k->name); + + if(visible) + options |= RRDF_FIELD_OPTS_VISIBLE; + + if(k->options & FACET_KEY_OPTION_MAIN_TEXT) + options |= RRDF_FIELD_OPTS_FULL_WIDTH | RRDF_FIELD_OPTS_WRAP; + + buffer_rrdf_table_add_field( + wb, field_id++, + k_dfe.name, k->name ? k->name : k_dfe.name, + RRDF_FIELD_TYPE_STRING, + RRDF_FIELD_VISUAL_VALUE, + RRDF_FIELD_TRANSFORM_NONE, 0, NULL, NAN, + RRDF_FIELD_SORT_ASCENDING, + NULL, + RRDF_FIELD_SUMMARY_COUNT, + k->values ? RRDF_FIELD_FILTER_FACET : RRDF_FIELD_FILTER_NONE, + options, + FACET_VALUE_UNSET); + } + dfe_done(k); + } + buffer_json_object_close(wb); // columns + + buffer_json_member_add_array(wb, "data"); + { + for(FACET_ROW *row = facets->base ; row ;row = row->next) { + buffer_json_add_array_item_array(wb); // each row + buffer_json_add_array_item_uint64(wb, row->usec); + + FACET_KEY *k; + dfe_start_read(facets->keys, k) + { + FACET_ROW_KEY_VALUE *rkv = dictionary_get(row->dict, k->name); + + if(unlikely(k->dynamic.cb)) { + if(unlikely(!rkv)) + rkv = dictionary_set(row->dict, k->name, NULL, sizeof(*rkv)); + + k->dynamic.cb(facets, wb, rkv, row, k->dynamic.data); + } + else + buffer_json_add_array_item_string(wb, rkv ? buffer_tostring(rkv->wb) : FACET_VALUE_UNSET); + } + dfe_done(k); + buffer_json_array_close(wb); // each row + } + } + buffer_json_array_close(wb); // data + + buffer_json_member_add_string(wb, "default_sort_column", "timestamp"); + buffer_json_member_add_array(wb, "default_charts"); + buffer_json_array_close(wb); + + buffer_json_member_add_object(wb, "items"); + { + buffer_json_member_add_uint64(wb, "evaluated", facets->operations.evaluated); + buffer_json_member_add_uint64(wb, "matched", facets->operations.matched); + buffer_json_member_add_uint64(wb, "returned", facets->items_to_return); + buffer_json_member_add_uint64(wb, "max_to_return", facets->max_items_to_return); + buffer_json_member_add_uint64(wb, "before", facets->operations.skips_before); + buffer_json_member_add_uint64(wb, "after", facets->operations.skips_after + facets->operations.shifts); + } + buffer_json_object_close(wb); // items + + buffer_json_member_add_object(wb, "stats"); + { + buffer_json_member_add_uint64(wb, "first", facets->operations.first); + buffer_json_member_add_uint64(wb, "forwards", facets->operations.forwards); + buffer_json_member_add_uint64(wb, "backwards", facets->operations.backwards); + buffer_json_member_add_uint64(wb, "skips_before", facets->operations.skips_before); + buffer_json_member_add_uint64(wb, "skips_after", facets->operations.skips_after); + buffer_json_member_add_uint64(wb, "prepends", facets->operations.prepends); + buffer_json_member_add_uint64(wb, "appends", facets->operations.appends); + buffer_json_member_add_uint64(wb, "shifts", facets->operations.shifts); + } + buffer_json_object_close(wb); // items + +} diff --git a/libnetdata/facets/facets.h b/libnetdata/facets/facets.h new file mode 100644 index 0000000000..310b8cdc98 --- /dev/null +++ b/libnetdata/facets/facets.h @@ -0,0 +1,62 @@ +#ifndef FACETS_H +#define FACETS_H 1 + +#include "../libnetdata.h" + +typedef enum __attribute__((packed)) { + FACET_KEY_OPTION_FACET = (1 << 0), // filterable values + FACET_KEY_OPTION_NO_FACET = (1 << 1), // non-filterable value + FACET_KEY_OPTION_STICKY = (1 << 2), // should be sticky in the table + FACET_KEY_OPTION_VISIBLE = (1 << 3), // should be in the default table + FACET_KEY_OPTION_FTS = (1 << 4), // the key is filterable by full text search (FTS) + FACET_KEY_OPTION_MAIN_TEXT = (1 << 5), // full width and wrap + FACET_KEY_OPTION_REORDER = (1 << 6), // give the key a new order id on first encounter +} FACET_KEY_OPTIONS; + +typedef struct facet_row_key_value { + const char *tmp; + BUFFER *wb; +} FACET_ROW_KEY_VALUE; + +typedef struct facet_row { + usec_t usec; + DICTIONARY *dict; + struct facet_row *prev, *next; +} FACET_ROW; + +typedef struct facets FACETS; +typedef struct facet_key FACET_KEY; + +#define FACET_STRING_HASH_SIZE 19 +void facets_string_hash(const char *src, char *out); + +typedef void (*facets_key_transformer_t)(FACETS *facets __maybe_unused, BUFFER *wb, void *data); +typedef void (*facet_dynamic_row_t)(FACETS *facets, BUFFER *wb, FACET_ROW_KEY_VALUE *rkv, FACET_ROW *row, void *data); +FACET_KEY *facets_register_dynamic_key(FACETS *facets, const char *key, FACET_KEY_OPTIONS options, facet_dynamic_row_t cb, void *data); +FACET_KEY *facets_register_key_transformation(FACETS *facets, const char *key, FACET_KEY_OPTIONS options, facets_key_transformer_t cb, void *data); + +typedef enum __attribute__((packed)) { + FACETS_OPTION_ALL_FACETS_VISIBLE = (1 << 0), // all facets, should be visible by default in the table + FACETS_OPTION_ALL_KEYS_FTS = (1 << 1), // all keys are searchable by full text search +} FACETS_OPTIONS; + +FACETS *facets_create(uint32_t items_to_return, usec_t anchor, FACETS_OPTIONS options, const char *visible_keys, const char *facet_keys, const char *non_facet_keys); +void facets_destroy(FACETS *facets); + +void facets_accepted_param(FACETS *facets, const char *param); + +void facets_rows_begin(FACETS *facets); +void facets_row_finished(FACETS *facets, usec_t usec); + +FACET_KEY *facets_register_key(FACETS *facets, const char *param, FACET_KEY_OPTIONS options); +void facets_set_query(FACETS *facets, const char *query); +void facets_set_items(FACETS *facets, uint32_t items); +void facets_set_anchor(FACETS *facets, usec_t anchor); +void facets_register_facet_filter(FACETS *facets, const char *key_id, char *value_ids, FACET_KEY_OPTIONS options); + +void facets_add_key_value(FACETS *facets, const char *key, const char *value); +void facets_add_key_value_length(FACETS *facets, const char *key, const char *value, size_t value_len); + +void facets_report(FACETS *facets, BUFFER *wb); + +#endif diff --git a/libnetdata/libnetdata.c b/libnetdata/libnetdata.c index d0a0b98a23..26582ffe2e 100644 --- a/libnetdata/libnetdata.c +++ b/libnetdata/libnetdata.c @@ -1966,3 +1966,135 @@ int hash256_string(const unsigned char *string, size_t size, char *hash) { EVP_MD_CTX_destroy(ctx); return 1; } + +// Returns 1 if an absolute period was requested or 0 if it was a relative period +bool rrdr_relative_window_to_absolute(time_t *after, time_t *before, time_t *now_ptr, bool unittest_running) { + time_t now = now_realtime_sec() - 1; + + if(now_ptr) + *now_ptr = now; + + int absolute_period_requested = -1; + long long after_requested, before_requested; + + before_requested = *before; + after_requested = *after; + + // allow relative for before (smaller than API_RELATIVE_TIME_MAX) + if(ABS(before_requested) <= API_RELATIVE_TIME_MAX) { + // if the user asked for a positive relative time, + // flip it to a negative + if(before_requested > 0) + before_requested = -before_requested; + + before_requested = now + before_requested; + absolute_period_requested = 0; + } + + // allow relative for after (smaller than API_RELATIVE_TIME_MAX) + if(ABS(after_requested) <= API_RELATIVE_TIME_MAX) { + if(after_requested > 0) + after_requested = -after_requested; + + // if the user didn't give an after, use the number of points + // to give a sane default + if(after_requested == 0) + after_requested = -600; + + // since the query engine now returns inclusive timestamps + // it is awkward to return 6 points when after=-5 is given + // so for relative queries we add 1 second, to give + // more predictable results to users. + after_requested = before_requested + after_requested + 1; + absolute_period_requested = 0; + } + + if(absolute_period_requested == -1) + absolute_period_requested = 1; + + // check if the parameters are flipped + if(after_requested > before_requested) { + long long t = before_requested; + before_requested = after_requested; + after_requested = t; + } + + // if the query requests future data + // shift the query back to be in the present time + // (this may also happen because of the rules above) + if(before_requested > now) { + long long delta = before_requested - now; + before_requested -= delta; + after_requested -= delta; + } + + time_t absolute_minimum_time = now - (10 * 365 * 86400); + time_t absolute_maximum_time = now + (1 * 365 * 86400); + + if (after_requested < absolute_minimum_time && !unittest_running) + after_requested = absolute_minimum_time; + + if (after_requested > absolute_maximum_time && !unittest_running) + after_requested = absolute_maximum_time; + + if (before_requested < absolute_minimum_time && !unittest_running) + before_requested = absolute_minimum_time; + + if (before_requested > absolute_maximum_time && !unittest_running) + before_requested = absolute_maximum_time; + + *before = before_requested; + *after = after_requested; + + return (absolute_period_requested != 1); +} + +int netdata_base64_decode(const char *encoded, char *decoded, size_t decoded_size) { + static const unsigned char base64_table[256] = { + ['A'] = 0, ['B'] = 1, ['C'] = 2, ['D'] = 3, ['E'] = 4, ['F'] = 5, ['G'] = 6, ['H'] = 7, + ['I'] = 8, ['J'] = 9, ['K'] = 10, ['L'] = 11, ['M'] = 12, ['N'] = 13, ['O'] = 14, ['P'] = 15, + ['Q'] = 16, ['R'] = 17, ['S'] = 18, ['T'] = 19, ['U'] = 20, ['V'] = 21, ['W'] = 22, ['X'] = 23, + ['Y'] = 24, ['Z'] = 25, ['a'] = 26, ['b'] = 27, ['c'] = 28, ['d'] = 29, ['e'] = 30, ['f'] = 31, + ['g'] = 32, ['h'] = 33, ['i'] = 34, ['j'] = 35, ['k'] = 36, ['l'] = 37, ['m'] = 38, ['n'] = 39, + ['o'] = 40, ['p'] = 41, ['q'] = 42, ['r'] = 43, ['s'] = 44, ['t'] = 45, ['u'] = 46, ['v'] = 47, + ['w'] = 48, ['x'] = 49, ['y'] = 50, ['z'] = 51, ['0'] = 52, ['1'] = 53, ['2'] = 54, ['3'] = 55, + ['4'] = 56, ['5'] = 57, ['6'] = 58, ['7'] = 59, ['8'] = 60, ['9'] = 61, ['+'] = 62, ['/'] = 63, + [0 ... '+' - 1] = 255, + ['+' + 1 ... '/' - 1] = 255, + ['9' + 1 ... 'A' - 1] = 255, + ['Z' + 1 ... 'a' - 1] = 255, + ['z' + 1 ... 255] = 255 + }; + + size_t count = 0; + unsigned int tmp = 0; + int i, bit; + + if (decoded_size < 1) + return 0; // Buffer size must be at least 1 for null termination + + for (i = 0, bit = 0; encoded[i]; i++) { + unsigned char value = base64_table[(unsigned char)encoded[i]]; + if (value > 63) + return -1; // Invalid character in input + + tmp = tmp << 6 | value; + if (++bit == 4) { + if (count + 3 >= decoded_size) break; // Stop decoding if buffer is full + decoded[count++] = (tmp >> 16) & 0xFF; + decoded[count++] = (tmp >> 8) & 0xFF; + decoded[count++] = tmp & 0xFF; + tmp = 0; + bit = 0; + } + } + + if (bit > 0 && count + 1 < decoded_size) { + tmp <<= 6 * (4 - bit); + if (bit > 2 && count + 1 < decoded_size) decoded[count++] = (tmp >> 16) & 0xFF; + if (bit > 3 && count + 1 < decoded_size) decoded[count++] = (tmp >> 8) & 0xFF; + } + + decoded[count] = '\0'; // Null terminate the output string + return count; +} diff --git a/libnetdata/libnetdata.h b/libnetdata/libnetdata.h index 53b93e1e1a..b8dedf5156 100644 --- a/libnetdata/libnetdata.h +++ b/libnetdata/libnetdata.h @@ -836,6 +836,7 @@ extern char *netdata_configured_host_prefix; #include "yaml.h" #include "http/http_defs.h" #include "gorilla/gorilla.h" +#include "facets/facets.h" #include "dyn_conf/dyn_conf.h" // BEWARE: this exists in alarm-notify.sh @@ -979,6 +980,14 @@ typedef enum { void timing_action(TIMING_ACTION action, TIMING_STEP step); int hash256_string(const unsigned char *string, size_t size, char *hash); + +extern bool unittest_running; +#define API_RELATIVE_TIME_MAX (3 * 365 * 86400) + +bool rrdr_relative_window_to_absolute(time_t *after, time_t *before, time_t *now_ptr, bool unittest_running); + +int netdata_base64_decode(const char *encoded, char *decoded, size_t decoded_size); + # ifdef __cplusplus } # endif diff --git a/libnetdata/socket/socket.h b/libnetdata/socket/socket.h index 8331ecbbe3..c4bd473609 100644 --- a/libnetdata/socket/socket.h +++ b/libnetdata/socket/socket.h @@ -11,27 +11,27 @@ typedef enum web_client_acl { WEB_CLIENT_ACL_NONE = (0), - WEB_CLIENT_ACL_NOCHECK = (0), // Don't check anything - this should work on all channels - WEB_CLIENT_ACL_DASHBOARD = (1 << 0), - WEB_CLIENT_ACL_REGISTRY = (1 << 1), - WEB_CLIENT_ACL_BADGE = (1 << 2), - WEB_CLIENT_ACL_MGMT = (1 << 3), - WEB_CLIENT_ACL_STREAMING = (1 << 4), - WEB_CLIENT_ACL_NETDATACONF = (1 << 5), - WEB_CLIENT_ACL_SSL_OPTIONAL = (1 << 6), - WEB_CLIENT_ACL_SSL_FORCE = (1 << 7), - WEB_CLIENT_ACL_SSL_DEFAULT = (1 << 8), - WEB_CLIENT_ACL_ACLK = (1 << 9), - WEB_CLIENT_ACL_WEBRTC = (1 << 10), - WEB_CLIENT_ACL_BEARER_OPTIONAL = (1 << 11), // allow unprotected access if bearer is not enabled in netdata - WEB_CLIENT_ACL_BEARER_REQUIRED = (1 << 12), // allow access only if a valid bearer is used + WEB_CLIENT_ACL_NOCHECK = (1 << 0), // Don't check anything - this should work on all channels + WEB_CLIENT_ACL_DASHBOARD = (1 << 1), + WEB_CLIENT_ACL_REGISTRY = (1 << 2), + WEB_CLIENT_ACL_BADGE = (1 << 3), + WEB_CLIENT_ACL_MGMT = (1 << 4), + WEB_CLIENT_ACL_STREAMING = (1 << 5), + WEB_CLIENT_ACL_NETDATACONF = (1 << 6), + WEB_CLIENT_ACL_SSL_OPTIONAL = (1 << 7), + WEB_CLIENT_ACL_SSL_FORCE = (1 << 8), + WEB_CLIENT_ACL_SSL_DEFAULT = (1 << 9), + WEB_CLIENT_ACL_ACLK = (1 << 10), + WEB_CLIENT_ACL_WEBRTC = (1 << 11), + WEB_CLIENT_ACL_BEARER_OPTIONAL = (1 << 12), // allow unprotected access if bearer is not enabled in netdata + WEB_CLIENT_ACL_BEARER_REQUIRED = (1 << 13), // allow access only if a valid bearer is used } WEB_CLIENT_ACL; #define WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC (WEB_CLIENT_ACL_DASHBOARD | WEB_CLIENT_ACL_ACLK | WEB_CLIENT_ACL_WEBRTC | WEB_CLIENT_ACL_BEARER_OPTIONAL) #define WEB_CLIENT_ACL_ACLK_WEBRTC_DASHBOARD_WITH_BEARER (WEB_CLIENT_ACL_DASHBOARD | WEB_CLIENT_ACL_ACLK | WEB_CLIENT_ACL_WEBRTC | WEB_CLIENT_ACL_BEARER_REQUIRED) #ifdef NETDATA_DEV_MODE -#define ACL_DEV_OPEN_ACCESS WEB_CLIENT_ACL_DASHBOARD +#define ACL_DEV_OPEN_ACCESS WEB_CLIENT_ACL_NOCHECK #else #define ACL_DEV_OPEN_ACCESS 0 #endif diff --git a/libnetdata/worker_utilization/worker_utilization.h b/libnetdata/worker_utilization/worker_utilization.h index 6745a010bc..cc3f826886 100644 --- a/libnetdata/worker_utilization/worker_utilization.h +++ b/libnetdata/worker_utilization/worker_utilization.h @@ -7,7 +7,7 @@ #define WORKER_UTILIZATION_MAX_JOB_TYPES 50 -typedef enum { +typedef enum __attribute__((packed)) { WORKER_METRIC_EMPTY = 0, WORKER_METRIC_IDLE_BUSY = 1, WORKER_METRIC_ABSOLUTE = 2, diff --git a/netdata-installer.sh b/netdata-installer.sh index 6161ca4bad..414ce7cd51 100755 --- a/netdata-installer.sh +++ b/netdata-installer.sh @@ -1290,6 +1290,11 @@ if [ "$(id -u)" -eq 0 ]; then run chown "root:${NETDATA_GROUP}" "${NETDATA_PREFIX}/usr/libexec/netdata/plugins.d/local-listeners" run chmod 4750 "${NETDATA_PREFIX}/usr/libexec/netdata/plugins.d/local-listeners" fi + + if [ -f "${NETDATA_PREFIX}/usr/libexec/netdata/plugins.d/systemd-journal.plugin" ]; then + run chown "root:${NETDATA_GROUP}" "${NETDATA_PREFIX}/usr/libexec/netdata/plugins.d/systemd-journal.plugin" + run chmod 4750 "${NETDATA_PREFIX}/usr/libexec/netdata/plugins.d/systemd-journal.plugin" + fi else # non-privileged user installation run chown "${NETDATA_USER}:${NETDATA_GROUP}" "${NETDATA_LOG_DIR}" diff --git a/registry/registry.c b/registry/registry.c index 4b6cb2bd1a..0393389ea3 100644 --- a/registry/registry.c +++ b/registry/registry.c @@ -58,7 +58,7 @@ static inline void registry_set_person_cookie(struct web_client *w, REGISTRY_PER static inline void registry_json_header(RRDHOST *host, struct web_client *w, const char *action, const char *status) { buffer_flush(w->response.data); w->response.data->content_type = CT_APPLICATION_JSON; - buffer_json_initialize(w->response.data, "\"", "\"", 0, true, false); + buffer_json_initialize(w->response.data, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_DEFAULT); buffer_json_member_add_string(w->response.data, "action", action); buffer_json_member_add_string(w->response.data, "status", status); buffer_json_member_add_string(w->response.data, "hostname", rrdhost_registry_hostname(host)); diff --git a/web/api/formatters/json_wrapper.c b/web/api/formatters/json_wrapper.c index 6a66cbcca6..52025c9fc2 100644 --- a/web/api/formatters/json_wrapper.c +++ b/web/api/formatters/json_wrapper.c @@ -874,7 +874,7 @@ void rrdr_json_wrapper_begin(RRDR *r, BUFFER *wb) { sq[0] = '"'; } - buffer_json_initialize(wb, kq, sq, 0, true, options & RRDR_OPTION_MINIFY); + buffer_json_initialize(wb, kq, sq, 0, true, (options & RRDR_OPTION_MINIFY) ? BUFFER_JSON_OPTIONS_MINIFY : BUFFER_JSON_OPTIONS_DEFAULT); buffer_json_member_add_uint64(wb, "api", 1); buffer_json_member_add_string(wb, "id", qt->id); @@ -1289,7 +1289,7 @@ void rrdr_json_wrapper_begin2(RRDR *r, BUFFER *wb) { sq[0] = '\''; } - buffer_json_initialize(wb, kq, sq, 0, true, options & RRDR_OPTION_MINIFY); + buffer_json_initialize(wb, kq, sq, 0, true, (options & RRDR_OPTION_MINIFY) ? BUFFER_JSON_OPTIONS_MINIFY : BUFFER_JSON_OPTIONS_DEFAULT); buffer_json_member_add_uint64(wb, "api", 2); if(options & RRDR_OPTION_DEBUG) { diff --git a/web/api/formatters/rrd2json.h b/web/api/formatters/rrd2json.h index ca3a41aae2..f0c0c39ba7 100644 --- a/web/api/formatters/rrd2json.h +++ b/web/api/formatters/rrd2json.h @@ -38,8 +38,6 @@ typedef enum { #define HOSTNAME_MAX 1024 -#define API_RELATIVE_TIME_MAX (3 * 365 * 86400) - #define DATASOURCE_FORMAT_JSON "json" #define DATASOURCE_FORMAT_JSON2 "json2" #define DATASOURCE_FORMAT_DATATABLE_JSON "datatable" diff --git a/web/api/queries/query.c b/web/api/queries/query.c index 8e1af4d63b..388dacee9a 100644 --- a/web/api/queries/query.c +++ b/web/api/queries/query.c @@ -2075,88 +2075,6 @@ static void rrd2rrdr_log_request_response_metadata(RRDR *r } #endif // NETDATA_INTERNAL_CHECKS -// Returns 1 if an absolute period was requested or 0 if it was a relative period -bool rrdr_relative_window_to_absolute(time_t *after, time_t *before, time_t *now_ptr) { - time_t now = now_realtime_sec() - 1; - - if(now_ptr) - *now_ptr = now; - - int absolute_period_requested = -1; - long long after_requested, before_requested; - - before_requested = *before; - after_requested = *after; - - // allow relative for before (smaller than API_RELATIVE_TIME_MAX) - if(ABS(before_requested) <= API_RELATIVE_TIME_MAX) { - // if the user asked for a positive relative time, - // flip it to a negative - if(before_requested > 0) - before_requested = -before_requested; - - before_requested = now + before_requested; - absolute_period_requested = 0; - } - - // allow relative for after (smaller than API_RELATIVE_TIME_MAX) - if(ABS(after_requested) <= API_RELATIVE_TIME_MAX) { - if(after_requested > 0) - after_requested = -after_requested; - - // if the user didn't give an after, use the number of points - // to give a sane default - if(after_requested == 0) - after_requested = -600; - - // since the query engine now returns inclusive timestamps - // it is awkward to return 6 points when after=-5 is given - // so for relative queries we add 1 second, to give - // more predictable results to users. - after_requested = before_requested + after_requested + 1; - absolute_period_requested = 0; - } - - if(absolute_period_requested == -1) - absolute_period_requested = 1; - - // check if the parameters are flipped - if(after_requested > before_requested) { - long long t = before_requested; - before_requested = after_requested; - after_requested = t; - } - - // if the query requests future data - // shift the query back to be in the present time - // (this may also happen because of the rules above) - if(before_requested > now) { - long long delta = before_requested - now; - before_requested -= delta; - after_requested -= delta; - } - - time_t absolute_minimum_time = now - (10 * 365 * 86400); - time_t absolute_maximum_time = now + (1 * 365 * 86400); - - if (after_requested < absolute_minimum_time && !unittest_running) - after_requested = absolute_minimum_time; - - if (after_requested > absolute_maximum_time && !unittest_running) - after_requested = absolute_maximum_time; - - if (before_requested < absolute_minimum_time && !unittest_running) - before_requested = absolute_minimum_time; - - if (before_requested > absolute_maximum_time && !unittest_running) - before_requested = absolute_maximum_time; - - *before = before_requested; - *after = after_requested; - - return (absolute_period_requested != 1); -} - // #define DEBUG_QUERY_LOGIC 1 #ifdef DEBUG_QUERY_LOGIC @@ -2283,7 +2201,7 @@ bool query_target_calculate_window(QUERY_TARGET *qt) { } // convert our before_wanted and after_wanted to absolute - rrdr_relative_window_to_absolute(&after_wanted, &before_wanted, NULL); + rrdr_relative_window_to_absolute(&after_wanted, &before_wanted, NULL, unittest_running); query_debug_log(":relative2absolute after %ld, before %ld", after_wanted, before_wanted); if (natural_points && (options & RRDR_OPTION_SELECTED_TIER) && tier > 0 && storage_tiers > 1) { diff --git a/web/api/queries/rrdr.h b/web/api/queries/rrdr.h index c4a1f83f23..e02e006752 100644 --- a/web/api/queries/rrdr.h +++ b/web/api/queries/rrdr.h @@ -206,8 +206,6 @@ RRDR *rrd2rrdr_legacy( RRDR *rrd2rrdr(ONEWAYALLOC *owa, struct query_target *qt); bool query_target_calculate_window(struct query_target *qt); -bool rrdr_relative_window_to_absolute(time_t *after, time_t *before, time_t *now_ptr); - #ifdef __cplusplus } #endif diff --git a/web/api/queries/weights.c b/web/api/queries/weights.c index 26d4555fa7..2782aef604 100644 --- a/web/api/queries/weights.c +++ b/web/api/queries/weights.c @@ -169,7 +169,7 @@ static size_t registered_results_to_json_charts(DICTIONARY *results, BUFFER *wb, size_t examined_dimensions, usec_t duration, WEIGHTS_STATS *stats) { - buffer_json_initialize(wb, "\"", "\"", 0, true, options & RRDR_OPTION_MINIFY); + buffer_json_initialize(wb, "\"", "\"", 0, true, (options & RRDR_OPTION_MINIFY) ? BUFFER_JSON_OPTIONS_MINIFY : BUFFER_JSON_OPTIONS_DEFAULT); results_header_to_json(results, wb, after, before, baseline_after, baseline_before, points, method, group, options, shifts, examined_dimensions, duration, stats); @@ -221,7 +221,7 @@ static size_t registered_results_to_json_contexts(DICTIONARY *results, BUFFER *w size_t examined_dimensions, usec_t duration, WEIGHTS_STATS *stats) { - buffer_json_initialize(wb, "\"", "\"", 0, true, options & RRDR_OPTION_MINIFY); + buffer_json_initialize(wb, "\"", "\"", 0, true, (options & RRDR_OPTION_MINIFY) ? BUFFER_JSON_OPTIONS_MINIFY : BUFFER_JSON_OPTIONS_DEFAULT); results_header_to_json(results, wb, after, before, baseline_after, baseline_before, points, method, group, options, shifts, examined_dimensions, duration, stats); @@ -739,7 +739,7 @@ static size_t registered_results_to_json_multinode_no_group_by( size_t examined_dimensions, struct query_weights_data *qwd, WEIGHTS_STATS *stats, struct query_versions *versions) { - buffer_json_initialize(wb, "\"", "\"", 0, true, options & RRDR_OPTION_MINIFY); + buffer_json_initialize(wb, "\"", "\"", 0, true, (options & RRDR_OPTION_MINIFY) ? BUFFER_JSON_OPTIONS_MINIFY : BUFFER_JSON_OPTIONS_DEFAULT); buffer_json_member_add_uint64(wb, "api", 2); results_header_to_json_v2(results, wb, qwd, after, before, baseline_after, baseline_before, @@ -958,7 +958,7 @@ static size_t registered_results_to_json_multinode_group_by( size_t examined_dimensions, struct query_weights_data *qwd, WEIGHTS_STATS *stats, struct query_versions *versions) { - buffer_json_initialize(wb, "\"", "\"", 0, true, options & RRDR_OPTION_MINIFY); + buffer_json_initialize(wb, "\"", "\"", 0, true, (options & RRDR_OPTION_MINIFY) ? BUFFER_JSON_OPTIONS_MINIFY : BUFFER_JSON_OPTIONS_DEFAULT); buffer_json_member_add_uint64(wb, "api", 2); results_header_to_json_v2(results, wb, qwd, after, before, baseline_after, baseline_before, @@ -1806,7 +1806,7 @@ int web_api_v12_weights(BUFFER *wb, QUERY_WEIGHTS_REQUEST *qwr) { } }; - if(!rrdr_relative_window_to_absolute(&qwr->after, &qwr->before, NULL)) + if(!rrdr_relative_window_to_absolute(&qwr->after, &qwr->before, NULL, false)) buffer_no_cacheable(wb); else buffer_cacheable(wb); @@ -1823,7 +1823,7 @@ int web_api_v12_weights(BUFFER *wb, QUERY_WEIGHTS_REQUEST *qwr) { if(qwr->baseline_before <= API_RELATIVE_TIME_MAX) qwr->baseline_before += qwr->after; - rrdr_relative_window_to_absolute(&qwr->baseline_after, &qwr->baseline_before, NULL); + rrdr_relative_window_to_absolute(&qwr->baseline_after, &qwr->baseline_before, NULL, false); if (qwr->baseline_before <= qwr->baseline_after) { resp = HTTP_RESP_BAD_REQUEST; diff --git a/web/api/web_api.c b/web/api/web_api.c index e2558b47c9..7a4704bd58 100644 --- a/web/api/web_api.c +++ b/web/api/web_api.c @@ -6,7 +6,7 @@ bool netdata_is_protected_by_bearer = false; // this is controlled by cloud, at DICTIONARY *netdata_authorized_bearers = NULL; static bool web_client_check_acl_and_bearer(struct web_client *w, WEB_CLIENT_ACL endpoint_acl) { - if(endpoint_acl == WEB_CLIENT_ACL_NOCHECK) + if(endpoint_acl == WEB_CLIENT_ACL_NONE || (endpoint_acl & WEB_CLIENT_ACL_NOCHECK)) // the endpoint is totally public return true; @@ -23,11 +23,24 @@ static bool web_client_check_acl_and_bearer(struct web_client *w, WEB_CLIENT_ACL // endpoint does not require a bearer return true; - if((w->acl & (WEB_CLIENT_ACL_ACLK|WEB_CLIENT_ACL_WEBRTC)) || api_check_bearer_token(w)) + if((w->acl & (WEB_CLIENT_ACL_ACLK|WEB_CLIENT_ACL_WEBRTC))) // the request is coming from ACLK or WEBRTC (authorized already), - // or we have a valid bearer on the request return true; + // at this point we need a bearer to serve the request + // either because: + // + // 1. WEB_CLIENT_ACL_BEARER_REQUIRED, or + // 2. netdata_is_protected_by_bearer == true + // + + BEARER_STATUS t = api_check_bearer_token(w); + if(t == BEARER_STATUS_AVAILABLE_AND_VALIDATED) + // we have a valid bearer on the request + return true; + + netdata_log_info("BEARER: bearer is required for request: code %d", t); + return false; } @@ -60,7 +73,7 @@ int web_client_api_request_vX(RRDHOST *host, struct web_client *w, char *url_pat return HTTP_RESP_BAD_REQUEST; } - if(unlikely(api_commands[i].acl != WEB_CLIENT_ACL_NOCHECK) && !(w->acl & api_commands[i].acl)) + if(unlikely(!web_client_check_acl_and_bearer(w, api_commands[i].acl))) return web_client_permission_denied(w); char *query_string = (char *)buffer_tostring(w->url_query_string_decoded); diff --git a/web/api/web_api.h b/web/api/web_api.h index 53bebfd269..f7ae45ad0c 100644 --- a/web/api/web_api.h +++ b/web/api/web_api.h @@ -11,9 +11,19 @@ extern bool netdata_is_protected_by_bearer; extern DICTIONARY *netdata_authorized_bearers; -bool api_check_bearer_token(struct web_client *w); -bool extract_bearer_token_from_request(struct web_client *w, char *dst, size_t dst_len); -void bearer_tokens_init(void); +typedef enum __attribute__((packed)) { + BEARER_STATUS_NO_BEARER_IN_HEADERS, + BEARER_STATUS_BEARER_DOES_NOT_FIT, + BEARER_STATUS_NOT_PARSABLE, + BEARER_STATUS_EXTRACTED_FROM_HEADER, + BEARER_STATUS_NO_BEARERS_DICTIONARY, + BEARER_STATUS_NOT_FOUND_IN_DICTIONARY, + BEARER_STATUS_EXPIRED, + BEARER_STATUS_AVAILABLE_AND_VALIDATED, +} BEARER_STATUS; + +BEARER_STATUS api_check_bearer_token(struct web_client *w); +BEARER_STATUS extract_bearer_token_from_request(struct web_client *w, char *dst, size_t dst_len); struct web_api_command { const char *command; diff --git a/web/api/web_api_v1.c b/web/api/web_api_v1.c index d730e42d78..60962413d6 100644 --- a/web/api/web_api_v1.c +++ b/web/api/web_api_v1.c @@ -941,7 +941,7 @@ inline int web_client_api_request_v1_registry(RRDHOST *host, struct web_client * char *cookie = strstr(w->response.data->buffer, NETDATA_REGISTRY_COOKIE_NAME "="); if(cookie) strncpyz(person_guid, &cookie[sizeof(NETDATA_REGISTRY_COOKIE_NAME)], UUID_STR_LEN - 1); - else if(!extract_bearer_token_from_request(w, person_guid, sizeof(person_guid))) + else if(extract_bearer_token_from_request(w, person_guid, sizeof(person_guid)) != BEARER_STATUS_EXTRACTED_FROM_HEADER) person_guid[0] = '\0'; char action = '\0'; @@ -1199,7 +1199,7 @@ static void host_collectors(RRDHOST *host, BUFFER *wb) { extern int aclk_connected; inline int web_client_api_request_v1_info_fill_buffer(RRDHOST *host, BUFFER *wb) { - buffer_json_initialize(wb, "\"", "\"", 0, true, false); + buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_DEFAULT); buffer_json_member_add_string(wb, "version", rrdhost_program_version(host)); buffer_json_member_add_string(wb, "uid", host->machine_guid); @@ -1319,7 +1319,7 @@ int web_client_api_request_v1_ml_info(RRDHOST *host, struct web_client *w, char buffer_flush(wb); wb->content_type = CT_APPLICATION_JSON; - buffer_json_initialize(wb, "\"", "\"", 0, true, false); + buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_DEFAULT); ml_host_get_detection_info(host, wb); buffer_json_finalize(wb); @@ -1424,7 +1424,7 @@ int web_client_api_request_v1_functions(RRDHOST *host, struct web_client *w, cha wb->content_type = CT_APPLICATION_JSON; buffer_no_cacheable(wb); - buffer_json_initialize(wb, "\"", "\"", 0, true, false); + buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_DEFAULT); host_functions2json(host, wb); buffer_json_finalize(wb); diff --git a/web/api/web_api_v2.c b/web/api/web_api_v2.c index e9b8d801d0..850282121e 100644 --- a/web/api/web_api_v2.c +++ b/web/api/web_api_v2.c @@ -54,13 +54,13 @@ static time_t bearer_get_token(uuid_t *uuid) { #define HTTP_REQUEST_AUTHORIZATION_BEARER "\r\nAuthorization: Bearer " -bool extract_bearer_token_from_request(struct web_client *w, char *dst, size_t dst_len) { +BEARER_STATUS extract_bearer_token_from_request(struct web_client *w, char *dst, size_t dst_len) { const char *req = buffer_tostring(w->response.data); size_t req_len = buffer_strlen(w->response.data); const char *bearer = strcasestr(req, HTTP_REQUEST_AUTHORIZATION_BEARER); if(!bearer) - return false; + return BEARER_STATUS_NO_BEARER_IN_HEADERS; const char *token_start = bearer + sizeof(HTTP_REQUEST_AUTHORIZATION_BEARER) - 1; @@ -69,26 +69,33 @@ bool extract_bearer_token_from_request(struct web_client *w, char *dst, size_t d const char *token_end = token_start + UUID_STR_LEN - 1 + 2; if (token_end > req + req_len) - return false; + return BEARER_STATUS_BEARER_DOES_NOT_FIT; strncpyz(dst, token_start, dst_len - 1); uuid_t uuid; if (uuid_parse(dst, uuid) != 0) - return false; + return BEARER_STATUS_NOT_PARSABLE; - return true; + return BEARER_STATUS_EXTRACTED_FROM_HEADER; } -bool api_check_bearer_token(struct web_client *w) { +BEARER_STATUS api_check_bearer_token(struct web_client *w) { if(!netdata_authorized_bearers) - return false; + return BEARER_STATUS_NO_BEARERS_DICTIONARY; char token[UUID_STR_LEN]; - if(!extract_bearer_token_from_request(w, token, sizeof(token))) - return false; + BEARER_STATUS t = extract_bearer_token_from_request(w, token, sizeof(token)); + if(t != BEARER_STATUS_EXTRACTED_FROM_HEADER) + return t; struct bearer_token *z = dictionary_get(netdata_authorized_bearers, token); - return z && z->expires_s > now_monotonic_sec(); + if(!z) + return BEARER_STATUS_NOT_FOUND_IN_DICTIONARY; + + if(z->expires_s < now_monotonic_sec()) + return BEARER_STATUS_EXPIRED; + + return BEARER_STATUS_AVAILABLE_AND_VALIDATED; } static bool verify_agent_uuids(const char *machine_guid, const char *node_id, const char *claim_id) { @@ -153,7 +160,7 @@ int api_v2_bearer_protection(RRDHOST *host __maybe_unused, struct web_client *w BUFFER *wb = w->response.data; buffer_flush(wb); - buffer_json_initialize(wb, "\"", "\"", 0, true, false); + buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_DEFAULT); buffer_json_member_add_boolean(wb, "bearer_protection", netdata_is_protected_by_bearer); buffer_json_finalize(wb); @@ -192,7 +199,7 @@ int api_v2_bearer_token(RRDHOST *host __maybe_unused, struct web_client *w __may BUFFER *wb = w->response.data; buffer_flush(wb); - buffer_json_initialize(wb, "\"", "\"", 0, true, false); + buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_DEFAULT); buffer_json_member_add_string(wb, "mg", localhost->machine_guid); buffer_json_member_add_boolean(wb, "bearer_protection", netdata_is_protected_by_bearer); buffer_json_member_add_uuid(wb, "token", &uuid); diff --git a/web/rtc/webrtc.c b/web/rtc/webrtc.c index 331326c84f..45e5e0dac6 100644 --- a/web/rtc/webrtc.c +++ b/web/rtc/webrtc.c @@ -304,7 +304,7 @@ static void webrtc_execute_api_request(WEBRTC_DC *chan, const char *request, siz web_client_timeout_checkpoint_set(w, 0); web_client_decode_path_and_query_string(w, path); path = (char *)buffer_tostring(w->url_path_decoded); - w->response.code = (short)web_client_api_request_with_node_selection(rrdb.localhost, w, path); + w->response.code = (short)web_client_api_request_with_node_selection(localhost, w, path); web_client_timeout_checkpoint_response_ready(w, NULL); size_t sent_bytes = 0; @@ -643,7 +643,7 @@ int webrtc_new_connection(const char *sdp, BUFFER *wb) { } buffer_flush(wb); - buffer_json_initialize(wb, "\"", "\"", 0, true, false); + buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_DEFAULT); wb->content_type = CT_APPLICATION_JSON; WEBRTC_CONN *conn = webrtc_create_connection();