--- /dev/null
+/*#############################################################################
+# #
+# telemetryd - The IPFire Telemetry Collection Service #
+# Copyright (C) 2025 IPFire Development Team #
+# #
+# 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 <http://www.gnu.org/licenses/>. #
+# #
+#############################################################################*/
+
+#ifdef BUILD_SOURCE_KNOT_RESOLVER
+
+#include <errno.h>
+#include <limits.h>
+
+#include "../command.h"
+#include "../ctx.h"
+#include "../source.h"
+#include "../string.h"
+#include "knot-resolver.h"
+
+typedef struct knot_resolver_metric {
+ sd_json_variant* json;
+ const char* key;
+ const char* field;
+} knot_resolver_metric;
+
+static int knot_resolver_parse_worker(td_ctx* ctx,
+ td_metrics* metrics, sd_json_variant* json) {
+ sd_json_variant* request = NULL;
+ sd_json_variant* answer = NULL;
+ sd_json_variant* query = NULL;
+ int r = 0;
+
+ // Fetch the request object
+ request = sd_json_variant_by_key(json, "request");
+ if (!request)
+ return -EBADMSG;
+
+ // Fetch the answer object
+ answer = sd_json_variant_by_key(json, "answer");
+ if (!answer)
+ return -EBADMSG;
+
+ // Fetch the query object
+ query = sd_json_variant_by_key(json, "query");
+ if (!query)
+ return -EBADMSG;
+
+ const knot_resolver_metric keys[] = {
+ // Query
+ { query, "dnssec", "query_dnssec", },
+ { query, "edns", "query_edns", },
+
+ // Request
+ { request, "total", "request_total", },
+ { request, "total6", "request_total6", },
+ { request, "total4", "request_total4", },
+ { request, "internal", "request_internal", },
+ { request, "udp", "request_udp", },
+ { request, "udp6", "request_udp6", },
+ { request, "udp4", "request_udp4", },
+ { request, "tcp", "request_tcp", },
+ { request, "tcp6", "request_tcp6", },
+ { request, "tcp4", "request_tcp4", },
+ { request, "dot", "request_dot", },
+ { request, "dot6", "request_dot6", },
+ { request, "dot4", "request_dot4", },
+ { request, "doh", "request_doh", },
+ { request, "doh6", "request_doh6", },
+ { request, "doh4", "request_doh4", },
+ { request, "doq", "request_doq", },
+ { request, "doq6", "request_doq6", },
+ { request, "doq4", "request_doq4", },
+ { request, "xdp", "request_xdp", },
+ { request, "xdp6", "request_xdp6", },
+ { request, "xdp4", "request_xdp4", },
+
+ // Answer
+ { answer, "total", "answer_total", },
+ { answer, "noerror", "answer_noerror", },
+ { answer, "nxdomain", "answer_nxdomain", },
+ { answer, "servfail", "answer_servfail", },
+ { answer, "nodata", "answer_nodata", },
+ { answer, "cached", "answer_cached", },
+ { answer, "stale", "answer_stale", },
+
+ // Answer Flags
+ { answer, "aa", "answer_aa", },
+ { answer, "ad", "answer_ad", },
+ { answer, "cd", "answer_cd", },
+ { answer, "do", "answer_do", },
+ { answer, "ra", "answer_ra", },
+ { answer, "rd", "answer_rd", },
+ { answer, "tc", "answer_tc", },
+ { answer, "edns0", "answer_edns0", },
+
+ // Answer Response Time
+ { answer, "1ms", "answer_1ms", },
+ { answer, "10ms", "answer_10ms", },
+ { answer, "50ms", "answer_50ms", },
+ { answer, "100ms", "answer_100ms", },
+ { answer, "250ms", "answer_250ms", },
+ { answer, "500ms", "answer_500ms", },
+ { answer, "1000ms", "answer_1000ms", },
+ { answer, "1500ms", "answer_1500ms", },
+ { answer, "slow", "answer_slow", },
+ { answer, "sum_ms", "answer_sum_ms", },
+
+ { NULL },
+ };
+
+ // Extract all metrics from JSON and push them into our metrics object
+ for (const knot_resolver_metric* key = keys; key->field; key++) {
+ r = td_metrics_push_uint64_from_json(metrics,
+ key->field, key->json, key->key);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static int knot_resolver_parse_metrics(td_ctx* ctx,
+ td_metrics* metrics, sd_json_variant* json) {
+ sd_json_variant* worker = NULL;
+ char name[NAME_MAX];
+ int r;
+
+ int i = 0;
+
+ for (;;) {
+ // Format the field name
+ r = td_string_format(name, "kresd:kresd%d", i++);
+ if (r < 0)
+ return r;
+
+ // Fetch the worker object
+ worker = sd_json_variant_by_key(json, name);
+ if (!worker)
+ break;
+
+ // Parse the worker
+ r = knot_resolver_parse_worker(ctx, metrics, worker);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static int knot_resolver_on_success(td_ctx* ctx,
+ int rc, td_file* stdout, void* data) {
+ sd_json_variant* json = NULL;
+ td_metrics* metrics = NULL;
+ td_source* source = data;
+ int r;
+
+ // Parse the output as JSON
+ r = td_file_parse_json(stdout, &json, 0);
+ if (r < 0)
+ goto ERROR;
+
+ // Create a new metrics object
+ r = td_source_create_metrics(source, &metrics, NULL);
+ if (r < 0)
+ goto ERROR;
+
+ // Parse the message
+ r = knot_resolver_parse_metrics(ctx, metrics, json);
+ if (r < 0)
+ goto ERROR;
+
+ // Submit all collected metrics
+ r = td_source_submit_metrics(source, metrics);
+
+ERROR:
+ if (json)
+ sd_json_variant_unref(json);
+ if (metrics)
+ td_metrics_unref(metrics);
+
+ return r;
+}
+
+static int knot_resolver_heartbeat(td_ctx* ctx, td_source* source) {
+ // Run kresctl to fetch metrics
+ const char* argv[] = {
+ "kresctl", "metrics", NULL,
+ };
+
+ return td_source_run_command(source, NULL, argv, knot_resolver_on_success, source);
+}
+
+const td_source_impl knot_resolver_source = {
+ .name = "knot-resolver",
+
+ // RRD Data Sources
+ .rrd_dss = {
+ // Query
+ { "query_dnssec", "DERIVE", 0, -1, },
+ { "query_edns", "DERIVE", 0, -1, },
+
+ // Request
+ { "request_total", "DERIVE", 0, -1, },
+ { "request_total6", "DERIVE", 0, -1, },
+ { "request_total4", "DERIVE", 0, -1, },
+ { "request_internal", "DERIVE", 0, -1, },
+ { "request_udp", "DERIVE", 0, -1, },
+ { "request_udp6", "DERIVE", 0, -1, },
+ { "request_udp4", "DERIVE", 0, -1, },
+ { "request_tcp", "DERIVE", 0, -1, },
+ { "request_tcp6", "DERIVE", 0, -1, },
+ { "request_tcp4", "DERIVE", 0, -1, },
+ { "request_dot", "DERIVE", 0, -1, },
+ { "request_dot6", "DERIVE", 0, -1, },
+ { "request_dot4", "DERIVE", 0, -1, },
+ { "request_doh", "DERIVE", 0, -1, },
+ { "request_doh6", "DERIVE", 0, -1, },
+ { "request_doh4", "DERIVE", 0, -1, },
+ { "request_doq", "DERIVE", 0, -1, },
+ { "request_doq6", "DERIVE", 0, -1, },
+ { "request_doq4", "DERIVE", 0, -1, },
+ { "request_xdp", "DERIVE", 0, -1, },
+ { "request_xdp6", "DERIVE", 0, -1, },
+ { "request_xdp4", "DERIVE", 0, -1, },
+
+ // Answer
+ { "answer_total", "DERIVE", 0, -1, },
+ { "answer_noerror", "DERIVE", 0, -1, },
+ { "answer_nxdomain", "DERIVE", 0, -1, },
+ { "answer_servfail", "DERIVE", 0, -1, },
+ { "answer_nodata", "DERIVE", 0, -1, },
+ { "answer_cached", "DERIVE", 0, -1, },
+ { "answer_stale", "DERIVE", 0, -1, },
+
+ // Answer Flags
+ { "answer_aa", "DERIVE", 0, -1, },
+ { "answer_ad", "DERIVE", 0, -1, },
+ { "answer_cd", "DERIVE", 0, -1, },
+ { "answer_do", "DERIVE", 0, -1, },
+ { "answer_ra", "DERIVE", 0, -1, },
+ { "answer_rd", "DERIVE", 0, -1, },
+ { "answer_tc", "DERIVE", 0, -1, },
+ { "answer_edns0", "DERIVE", 0, -1, },
+
+ // Answer Response Time
+ { "answer_1ms", "DERIVE", 0, -1, },
+ { "answer_10ms", "DERIVE", 0, -1, },
+ { "answer_50ms", "DERIVE", 0, -1, },
+ { "answer_100ms", "DERIVE", 0, -1, },
+ { "answer_250ms", "DERIVE", 0, -1, },
+ { "answer_500ms", "DERIVE", 0, -1, },
+ { "answer_1000ms", "DERIVE", 0, -1, },
+ { "answer_1500ms", "DERIVE", 0, -1, },
+ { "answer_slow", "DERIVE", 0, -1, },
+ { "answer_sum_ms", "DERIVE", 0, -1, },
+
+ { NULL },
+ },
+
+ // Methods
+ .heartbeat = knot_resolver_heartbeat,
+};
+
+#endif /* BUILD_SOURCE_KNOT_RESOLVER */