summaryrefslogtreecommitdiffstats
path: root/src/knot/common/stats.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/knot/common/stats.c')
-rw-r--r--src/knot/common/stats.c309
1 files changed, 309 insertions, 0 deletions
diff --git a/src/knot/common/stats.c b/src/knot/common/stats.c
new file mode 100644
index 0000000..2b8cb09
--- /dev/null
+++ b/src/knot/common/stats.c
@@ -0,0 +1,309 @@
+/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <inttypes.h>
+#include <sys/stat.h>
+#include <time.h>
+#include <unistd.h>
+#include <urcu.h>
+
+#include "contrib/files.h"
+#include "knot/common/stats.h"
+#include "knot/common/log.h"
+#include "knot/nameserver/query_module.h"
+
+struct {
+ bool active_dumper;
+ pthread_t dumper;
+ uint32_t timer;
+ server_t *server;
+} stats = { 0 };
+
+typedef struct {
+ FILE *fd;
+ const list_t *query_modules;
+ const knot_dname_t *zone;
+ bool zone_emitted;
+} dump_ctx_t;
+
+#define DUMP_STR(fd, level, name, ...) do { \
+ fprintf(fd, "%-.*s"name": %s\n", level, " ", ##__VA_ARGS__); \
+ } while (0)
+#define DUMP_CTR(fd, level, name, ...) do { \
+ fprintf(fd, "%-.*s"name": %"PRIu64"\n", level, " ", ##__VA_ARGS__); \
+ } while (0)
+
+uint64_t server_zone_count(server_t *server)
+{
+ return knot_zonedb_size(server->zone_db);
+}
+
+const stats_item_t server_stats[] = {
+ { "zone-count", server_zone_count },
+ { 0 }
+};
+
+uint64_t stats_get_counter(uint64_t **stats_vals, uint32_t offset, unsigned threads)
+{
+ uint64_t res = 0;
+ for (unsigned i = 0; i < threads; i++) {
+ res += ATOMIC_GET(stats_vals[i][offset]);
+ }
+ return res;
+}
+
+static void dump_counters(FILE *fd, int level, mod_ctr_t *ctr, uint64_t **stats_vals, unsigned threads)
+{
+ for (uint32_t j = 0; j < ctr->count; j++) {
+ uint64_t counter = stats_get_counter(stats_vals, ctr->offset + j, threads);
+
+ // Skip empty counters.
+ if (counter == 0) {
+ continue;
+ }
+
+ if (ctr->idx_to_str != NULL) {
+ char *str = ctr->idx_to_str(j, ctr->count);
+ if (str != NULL) {
+ DUMP_CTR(fd, level, "%s", str, counter);
+ free(str);
+ }
+ } else {
+ DUMP_CTR(fd, level, "%u", j, counter);
+ }
+ }
+}
+
+static void dump_modules(dump_ctx_t *ctx)
+{
+ int level = 0;
+ knotd_mod_t *mod;
+ WALK_LIST(mod, *ctx->query_modules) {
+ // Skip modules without statistics.
+ if (mod->stats_count == 0) {
+ continue;
+ }
+
+ // Dump zone name.
+ if (ctx->zone != NULL) {
+ // Prevent from zone section override.
+ if (!ctx->zone_emitted) {
+ DUMP_STR(ctx->fd, 0, "zone", "");
+ ctx->zone_emitted = true;
+ }
+ level = 1;
+
+ knot_dname_txt_storage_t name;
+ if (knot_dname_to_str(name, ctx->zone, sizeof(name)) == NULL) {
+ return;
+ }
+ DUMP_STR(ctx->fd, level++, "\"%s\"", name, "");
+ } else {
+ level = 0;
+ }
+
+ unsigned threads = knotd_mod_threads(mod);
+
+ // Dump module counters.
+ DUMP_STR(ctx->fd, level, "%s", mod->id->name + 1, "");
+ for (int i = 0; i < mod->stats_count; i++) {
+ mod_ctr_t *ctr = mod->stats_info + i;
+ if (ctr->name == NULL) {
+ // Empty counter.
+ continue;
+ }
+ if (ctr->count == 1) {
+ // Simple counter.
+ uint64_t counter = stats_get_counter(mod->stats_vals,
+ ctr->offset, threads);
+ DUMP_CTR(ctx->fd, level + 1, "%s", ctr->name, counter);
+ } else {
+ // Array of counters.
+ DUMP_STR(ctx->fd, level + 1, "%s", ctr->name, "");
+ dump_counters(ctx->fd, level + 2, ctr, mod->stats_vals, threads);
+ }
+ }
+ }
+}
+
+static void zone_stats_dump(zone_t *zone, dump_ctx_t *ctx)
+{
+ if (EMPTY_LIST(zone->query_modules)) {
+ return;
+ }
+
+ ctx->query_modules = &zone->query_modules;
+ ctx->zone = zone->name;
+
+ dump_modules(ctx);
+}
+
+static void dump_to_file(FILE *fd, server_t *server)
+{
+ char date[64] = "";
+
+ // Get formatted current time string.
+ struct tm tm;
+ time_t now = time(NULL);
+ localtime_r(&now, &tm);
+ strftime(date, sizeof(date), KNOT_LOG_TIME_FORMAT, &tm);
+
+ // Get the server identity.
+ conf_val_t val = conf_get(conf(), C_SRV, C_IDENT);
+ const char *ident = conf_str(&val);
+ if (ident == NULL || ident[0] == '\0') {
+ ident = conf()->hostname;
+ }
+
+ // Dump record header.
+ fprintf(fd,
+ "---\n"
+ "time: %s\n"
+ "identity: %s\n",
+ date, ident);
+
+ // Dump server statistics.
+ DUMP_STR(fd, 0, "server", "");
+ for (const stats_item_t *item = server_stats; item->name != NULL; item++) {
+ DUMP_CTR(fd, 1, "%s", item->name, item->val(server));
+ }
+
+ dump_ctx_t ctx = {
+ .fd = fd,
+ .query_modules = conf()->query_modules,
+ };
+
+ // Dump global statistics.
+ dump_modules(&ctx);
+
+ // Dump zone statistics.
+ knot_zonedb_foreach(server->zone_db, zone_stats_dump, &ctx);
+}
+
+static void dump_stats(server_t *server)
+{
+ conf_t *pconf = conf();
+ conf_val_t val = conf_get(pconf, C_SRV, C_RUNDIR);
+ char *rundir = conf_abs_path(&val, NULL);
+ val = conf_get(pconf, C_STATS, C_FILE);
+ char *file_name = conf_abs_path(&val, rundir);
+ free(rundir);
+
+ val = conf_get(pconf, C_STATS, C_APPEND);
+ bool append = conf_bool(&val);
+
+ // Open or create output file.
+ FILE *fd = NULL;
+ char *tmp_name = NULL;
+ if (append) {
+ fd = fopen(file_name, "a");
+ if (fd == NULL) {
+ log_error("stats, failed to append file '%s' (%s)",
+ file_name, knot_strerror(knot_map_errno()));
+ free(file_name);
+ return;
+ }
+ } else {
+ int ret = open_tmp_file(file_name, &tmp_name, &fd,
+ S_IRUSR | S_IWUSR | S_IRGRP);
+ if (ret != KNOT_EOK) {
+ log_error("stats, failed to open file '%s' (%s)",
+ file_name, knot_strerror(ret));
+ free(file_name);
+ return;
+ }
+ }
+ assert(fd);
+
+ // Dump stats into the file.
+ dump_to_file(fd, server);
+
+ fflush(fd);
+ fclose(fd);
+
+ // Switch the file contents.
+ if (!append) {
+ int ret = rename(tmp_name, file_name);
+ if (ret != 0) {
+ log_error("stats, failed to access file '%s' (%s)",
+ file_name, knot_strerror(knot_map_errno()));
+ unlink(tmp_name);
+ }
+ free(tmp_name);
+ }
+
+ log_debug("stats, dumped into file '%s'", file_name);
+ free(file_name);
+}
+
+static void *dumper(void *data)
+{
+ rcu_register_thread();
+ while (true) {
+ assert(stats.timer > 0);
+ sleep(stats.timer);
+
+ pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
+ rcu_read_lock();
+ dump_stats(stats.server);
+ rcu_read_unlock();
+ pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
+ }
+ rcu_unregister_thread();
+ return NULL;
+}
+
+void stats_reconfigure(conf_t *conf, server_t *server)
+{
+ if (conf == NULL || server == NULL) {
+ return;
+ }
+
+ // Update server context.
+ stats.server = server;
+
+ conf_val_t val = conf_get(conf, C_STATS, C_TIMER);
+ stats.timer = conf_int(&val);
+ if (stats.timer > 0) {
+ // Check if dumping is already running.
+ if (stats.active_dumper) {
+ return;
+ }
+
+ int ret = pthread_create(&stats.dumper, NULL, dumper, NULL);
+ if (ret != 0) {
+ log_error("stats, failed to launch periodic dumping (%s)",
+ knot_strerror(knot_map_errno_code(ret)));
+ } else {
+ stats.active_dumper = true;
+ }
+ // Stop current dumping.
+ } else if (stats.active_dumper) {
+ pthread_cancel(stats.dumper);
+ pthread_join(stats.dumper, NULL);
+ stats.active_dumper = false;
+ }
+}
+
+void stats_deinit(void)
+{
+ if (stats.active_dumper) {
+ pthread_cancel(stats.dumper);
+ pthread_join(stats.dumper, NULL);
+ }
+
+ memset(&stats, 0, sizeof(stats));
+}