summaryrefslogtreecommitdiffstats
path: root/src/knot/modules/stats
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 15:24:08 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 15:24:08 +0000
commitf449f278dd3c70e479a035f50a9bb817a9b433ba (patch)
tree8ca2bfb785dda9bb4d573acdf9b42aea9cd51383 /src/knot/modules/stats
parentInitial commit. (diff)
downloadknot-f449f278dd3c70e479a035f50a9bb817a9b433ba.tar.xz
knot-f449f278dd3c70e479a035f50a9bb817a9b433ba.zip
Adding upstream version 3.2.6.upstream/3.2.6upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/knot/modules/stats')
-rw-r--r--src/knot/modules/stats/Makefile.inc13
-rw-r--r--src/knot/modules/stats/stats.c676
-rw-r--r--src/knot/modules/stats/stats.rst274
3 files changed, 963 insertions, 0 deletions
diff --git a/src/knot/modules/stats/Makefile.inc b/src/knot/modules/stats/Makefile.inc
new file mode 100644
index 0000000..8952d49
--- /dev/null
+++ b/src/knot/modules/stats/Makefile.inc
@@ -0,0 +1,13 @@
+knot_modules_stats_la_SOURCES = knot/modules/stats/stats.c
+EXTRA_DIST += knot/modules/stats/stats.rst
+
+if STATIC_MODULE_stats
+libknotd_la_SOURCES += $(knot_modules_stats_la_SOURCES)
+endif
+
+if SHARED_MODULE_stats
+knot_modules_stats_la_LDFLAGS = $(KNOTD_MOD_LDFLAGS)
+knot_modules_stats_la_CPPFLAGS = $(KNOTD_MOD_CPPFLAGS)
+knot_modules_stats_la_LIBADD = $(libcontrib_LIBS)
+pkglib_LTLIBRARIES += knot/modules/stats.la
+endif
diff --git a/src/knot/modules/stats/stats.c b/src/knot/modules/stats/stats.c
new file mode 100644
index 0000000..26262ac
--- /dev/null
+++ b/src/knot/modules/stats/stats.c
@@ -0,0 +1,676 @@
+/* Copyright (C) 2022 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 "contrib/macros.h"
+#include "contrib/wire_ctx.h"
+#include "knot/include/module.h"
+#include "knot/nameserver/xfr.h" // Dependency on qdata->extra!
+
+#define MOD_PROTOCOL "\x10""request-protocol"
+#define MOD_OPERATION "\x10""server-operation"
+#define MOD_REQ_BYTES "\x0D""request-bytes"
+#define MOD_RESP_BYTES "\x0E""response-bytes"
+#define MOD_EDNS "\x0D""edns-presence"
+#define MOD_FLAG "\x0D""flag-presence"
+#define MOD_RCODE "\x0D""response-code"
+#define MOD_REQ_EOPT "\x13""request-edns-option"
+#define MOD_RESP_EOPT "\x14""response-edns-option"
+#define MOD_NODATA "\x0C""reply-nodata"
+#define MOD_QTYPE "\x0A""query-type"
+#define MOD_QSIZE "\x0A""query-size"
+#define MOD_RSIZE "\x0A""reply-size"
+
+#define OTHER "other"
+
+const yp_item_t stats_conf[] = {
+ { MOD_PROTOCOL, YP_TBOOL, YP_VBOOL = { true } },
+ { MOD_OPERATION, YP_TBOOL, YP_VBOOL = { true } },
+ { MOD_REQ_BYTES, YP_TBOOL, YP_VBOOL = { true } },
+ { MOD_RESP_BYTES, YP_TBOOL, YP_VBOOL = { true } },
+ { MOD_EDNS, YP_TBOOL, YP_VNONE },
+ { MOD_FLAG, YP_TBOOL, YP_VNONE },
+ { MOD_RCODE, YP_TBOOL, YP_VBOOL = { true } },
+ { MOD_REQ_EOPT, YP_TBOOL, YP_VNONE },
+ { MOD_RESP_EOPT, YP_TBOOL, YP_VNONE },
+ { MOD_NODATA, YP_TBOOL, YP_VNONE },
+ { MOD_QTYPE, YP_TBOOL, YP_VNONE },
+ { MOD_QSIZE, YP_TBOOL, YP_VNONE },
+ { MOD_RSIZE, YP_TBOOL, YP_VNONE },
+ { NULL }
+};
+
+enum {
+ CTR_PROTOCOL,
+ CTR_OPERATION,
+ CTR_REQ_BYTES,
+ CTR_RESP_BYTES,
+ CTR_EDNS,
+ CTR_FLAG,
+ CTR_RCODE,
+ CTR_REQ_EOPT,
+ CTR_RESP_EOPT,
+ CTR_NODATA,
+ CTR_QTYPE,
+ CTR_QSIZE,
+ CTR_RSIZE,
+};
+
+typedef struct {
+ bool protocol;
+ bool operation;
+ bool req_bytes;
+ bool resp_bytes;
+ bool edns;
+ bool flag;
+ bool rcode;
+ bool req_eopt;
+ bool resp_eopt;
+ bool nodata;
+ bool qtype;
+ bool qsize;
+ bool rsize;
+} stats_t;
+
+typedef struct {
+ yp_name_t *conf_name;
+ size_t conf_offset;
+ uint32_t count;
+ knotd_mod_idx_to_str_f fcn;
+} ctr_desc_t;
+
+enum {
+ OPERATION_QUERY = 0,
+ OPERATION_UPDATE,
+ OPERATION_NOTIFY,
+ OPERATION_AXFR,
+ OPERATION_IXFR,
+ OPERATION_INVALID,
+ OPERATION__COUNT
+};
+
+static char *operation_to_str(uint32_t idx, uint32_t count)
+{
+ switch (idx) {
+ case OPERATION_QUERY: return strdup("query");
+ case OPERATION_UPDATE: return strdup("update");
+ case OPERATION_NOTIFY: return strdup("notify");
+ case OPERATION_AXFR: return strdup("axfr");
+ case OPERATION_IXFR: return strdup("ixfr");
+ case OPERATION_INVALID: return strdup("invalid");
+ default: assert(0); return NULL;
+ }
+}
+
+enum {
+ PROTOCOL_UDP4 = 0,
+ PROTOCOL_TCP4,
+ PROTOCOL_QUIC4,
+ PROTOCOL_UDP6,
+ PROTOCOL_TCP6,
+ PROTOCOL_QUIC6,
+ PROTOCOL_UDP4_XDP,
+ PROTOCOL_TCP4_XDP,
+ PROTOCOL_QUIC4_XDP,
+ PROTOCOL_UDP6_XDP,
+ PROTOCOL_TCP6_XDP,
+ PROTOCOL_QUIC6_XDP,
+ PROTOCOL__COUNT
+};
+
+static char *protocol_to_str(uint32_t idx, uint32_t count)
+{
+ switch (idx) {
+ case PROTOCOL_UDP4: return strdup("udp4");
+ case PROTOCOL_TCP4: return strdup("tcp4");
+ case PROTOCOL_QUIC4: return strdup("quic4");
+ case PROTOCOL_UDP6: return strdup("udp6");
+ case PROTOCOL_TCP6: return strdup("tcp6");
+ case PROTOCOL_QUIC6: return strdup("quic6");
+ case PROTOCOL_UDP4_XDP: return strdup("udp4-xdp");
+ case PROTOCOL_TCP4_XDP: return strdup("tcp4-xdp");
+ case PROTOCOL_QUIC4_XDP: return strdup("quic4-xdp");
+ case PROTOCOL_UDP6_XDP: return strdup("udp6-xdp");
+ case PROTOCOL_TCP6_XDP: return strdup("tcp6-xdp");
+ case PROTOCOL_QUIC6_XDP: return strdup("quic6-xdp");
+ default: assert(0); return NULL;
+ }
+}
+
+enum {
+ REQ_BYTES_QUERY = 0,
+ REQ_BYTES_UPDATE,
+ REQ_BYTES_OTHER,
+ REQ_BYTES__COUNT
+};
+
+static char *req_bytes_to_str(uint32_t idx, uint32_t count)
+{
+ switch (idx) {
+ case REQ_BYTES_QUERY: return strdup("query");
+ case REQ_BYTES_UPDATE: return strdup("update");
+ case REQ_BYTES_OTHER: return strdup(OTHER);
+ default: assert(0); return NULL;
+ }
+}
+
+enum {
+ RESP_BYTES_REPLY = 0,
+ RESP_BYTES_TRANSFER,
+ RESP_BYTES_OTHER,
+ RESP_BYTES__COUNT
+};
+
+static char *resp_bytes_to_str(uint32_t idx, uint32_t count)
+{
+ switch (idx) {
+ case RESP_BYTES_REPLY: return strdup("reply");
+ case RESP_BYTES_TRANSFER: return strdup("transfer");
+ case RESP_BYTES_OTHER: return strdup(OTHER);
+ default: assert(0); return NULL;
+ }
+}
+
+enum {
+ EDNS_REQ = 0,
+ EDNS_RESP,
+ EDNS__COUNT
+};
+
+static char *edns_to_str(uint32_t idx, uint32_t count)
+{
+ switch (idx) {
+ case EDNS_REQ: return strdup("request");
+ case EDNS_RESP: return strdup("response");
+ default: assert(0); return NULL;
+ }
+}
+
+enum {
+ FLAG_DO = 0,
+ FLAG_TC,
+ FLAG__COUNT
+};
+
+static char *flag_to_str(uint32_t idx, uint32_t count)
+{
+ switch (idx) {
+ case FLAG_TC: return strdup("TC");
+ case FLAG_DO: return strdup("DO");
+ default: assert(0); return NULL;
+ }
+}
+
+enum {
+ NODATA_A = 0,
+ NODATA_AAAA,
+ NODATA_OTHER,
+ NODATA__COUNT
+};
+
+static char *nodata_to_str(uint32_t idx, uint32_t count)
+{
+ switch (idx) {
+ case NODATA_A: return strdup("A");
+ case NODATA_AAAA: return strdup("AAAA");
+ case NODATA_OTHER: return strdup(OTHER);
+ default: assert(0); return NULL;
+ }
+}
+
+#define RCODE_BADSIG 15 // Unassigned code internally used for BADSIG.
+#define RCODE_OTHER (KNOT_RCODE_BADCOOKIE + 1) // Other RCODES.
+
+static char *rcode_to_str(uint32_t idx, uint32_t count)
+{
+ const knot_lookup_t *rcode = NULL;
+
+ switch (idx) {
+ case RCODE_BADSIG:
+ rcode = knot_lookup_by_id(knot_tsig_rcode_names, KNOT_RCODE_BADSIG);
+ break;
+ case RCODE_OTHER:
+ return strdup(OTHER);
+ default:
+ rcode = knot_lookup_by_id(knot_rcode_names, idx);
+ break;
+ }
+
+ if (rcode != NULL) {
+ return strdup(rcode->name);
+ } else {
+ return NULL;
+ }
+}
+
+#define EOPT_OTHER (KNOT_EDNS_MAX_OPTION_CODE + 1)
+#define req_eopt_to_str eopt_to_str
+#define resp_eopt_to_str eopt_to_str
+
+static char *eopt_to_str(uint32_t idx, uint32_t count)
+{
+ if (idx >= EOPT_OTHER) {
+ return strdup(OTHER);
+ }
+
+ char str[32];
+ if (knot_opt_code_to_string(idx, str, sizeof(str)) < 0) {
+ return NULL;
+ } else {
+ return strdup(str);
+ }
+}
+
+enum {
+ QTYPE_OTHER = 0,
+ QTYPE_MIN1 = 1,
+ QTYPE_MAX1 = 65,
+ QTYPE_MIN2 = 99,
+ QTYPE_MAX2 = 110,
+ QTYPE_MIN3 = 255,
+ QTYPE_MAX3 = 260,
+ QTYPE_SHIFT2 = QTYPE_MIN2 - QTYPE_MAX1 - 1,
+ QTYPE_SHIFT3 = QTYPE_SHIFT2 + QTYPE_MIN3 - QTYPE_MAX2 - 1,
+ QTYPE__COUNT = QTYPE_MAX3 - QTYPE_SHIFT3 + 1
+};
+
+static char *qtype_to_str(uint32_t idx, uint32_t count)
+{
+ if (idx == QTYPE_OTHER) {
+ return strdup(OTHER);
+ }
+
+ uint16_t qtype;
+
+ if (idx <= QTYPE_MAX1) {
+ qtype = idx;
+ assert(qtype >= QTYPE_MIN1 && qtype <= QTYPE_MAX1);
+ } else if (idx <= QTYPE_MAX2 - QTYPE_SHIFT2) {
+ qtype = idx + QTYPE_SHIFT2;
+ assert(qtype >= QTYPE_MIN2 && qtype <= QTYPE_MAX2);
+ } else {
+ qtype = idx + QTYPE_SHIFT3;
+ assert(qtype >= QTYPE_MIN3 && qtype <= QTYPE_MAX3);
+ }
+
+ char str[32];
+ if (knot_rrtype_to_string(qtype, str, sizeof(str)) < 0) {
+ return NULL;
+ } else {
+ return strdup(str);
+ }
+}
+
+#define BUCKET_SIZE 16
+#define QSIZE_MAX_IDX (288 / BUCKET_SIZE)
+#define RSIZE_MAX_IDX (4096 / BUCKET_SIZE)
+
+static char *size_to_str(uint32_t idx, uint32_t count)
+{
+ char str[16];
+
+ int ret;
+ if (idx < count - 1) {
+ ret = snprintf(str, sizeof(str), "%u-%u", idx * BUCKET_SIZE,
+ (idx + 1) * BUCKET_SIZE - 1);
+ } else {
+ ret = snprintf(str, sizeof(str), "%u-65535", idx * BUCKET_SIZE);
+ }
+
+ if (ret <= 0 || (size_t)ret >= sizeof(str)) {
+ return NULL;
+ } else {
+ return strdup(str);
+ }
+}
+
+static char *qsize_to_str(uint32_t idx, uint32_t count)
+{
+ return size_to_str(idx, count);
+}
+
+static char *rsize_to_str(uint32_t idx, uint32_t count)
+{
+ return size_to_str(idx, count);
+}
+
+static const ctr_desc_t ctr_descs[] = {
+ #define item(macro, name, count) \
+ [CTR_##macro] = { MOD_##macro, offsetof(stats_t, name), (count), name##_to_str }
+ item(PROTOCOL, protocol, PROTOCOL__COUNT),
+ item(OPERATION, operation, OPERATION__COUNT),
+ item(REQ_BYTES, req_bytes, REQ_BYTES__COUNT),
+ item(RESP_BYTES, resp_bytes, RESP_BYTES__COUNT),
+ item(EDNS, edns, EDNS__COUNT),
+ item(FLAG, flag, FLAG__COUNT),
+ item(RCODE, rcode, RCODE_OTHER + 1),
+ item(REQ_EOPT, req_eopt, EOPT_OTHER + 1),
+ item(RESP_EOPT, resp_eopt, EOPT_OTHER + 1),
+ item(NODATA, nodata, NODATA__COUNT),
+ item(QTYPE, qtype, QTYPE__COUNT),
+ item(QSIZE, qsize, QSIZE_MAX_IDX + 1),
+ item(RSIZE, rsize, RSIZE_MAX_IDX + 1),
+ { NULL }
+};
+
+static void incr_edns_option(knotd_mod_t *mod, unsigned thr_id, const knot_pkt_t *pkt, unsigned ctr_name)
+{
+ if (!knot_pkt_has_edns(pkt)) {
+ return;
+ }
+
+ knot_rdata_t *rdata = pkt->opt_rr->rrs.rdata;
+ if (rdata == NULL || rdata->len == 0) {
+ return;
+ }
+
+ wire_ctx_t wire = wire_ctx_init_const(rdata->data, rdata->len);
+ while (wire_ctx_available(&wire) > 0) {
+ uint16_t opt_code = wire_ctx_read_u16(&wire);
+ uint16_t opt_len = wire_ctx_read_u16(&wire);
+ wire_ctx_skip(&wire, opt_len);
+ if (wire.error != KNOT_EOK) {
+ break;
+ }
+ knotd_mod_stats_incr(mod, thr_id, ctr_name, MIN(opt_code, EOPT_OTHER), 1);
+ }
+}
+
+static knotd_state_t update_counters(knotd_state_t state, knot_pkt_t *pkt,
+ knotd_qdata_t *qdata, knotd_mod_t *mod)
+{
+ assert(pkt && qdata);
+
+ stats_t *stats = knotd_mod_ctx(mod);
+
+ uint16_t operation;
+ unsigned xfr_packets = 0;
+ unsigned tid = qdata->params->thread_id;
+
+ // Get the server operation.
+ switch (qdata->type) {
+ case KNOTD_QUERY_TYPE_NORMAL:
+ operation = OPERATION_QUERY;
+ break;
+ case KNOTD_QUERY_TYPE_UPDATE:
+ operation = OPERATION_UPDATE;
+ break;
+ case KNOTD_QUERY_TYPE_NOTIFY:
+ operation = OPERATION_NOTIFY;
+ break;
+ case KNOTD_QUERY_TYPE_AXFR:
+ operation = OPERATION_AXFR;
+ if (qdata->extra->ext != NULL) {
+ xfr_packets = ((struct xfr_proc *)qdata->extra->ext)->stats.messages;
+ }
+ break;
+ case KNOTD_QUERY_TYPE_IXFR:
+ operation = OPERATION_IXFR;
+ if (qdata->extra->ext != NULL) {
+ xfr_packets = ((struct xfr_proc *)qdata->extra->ext)->stats.messages;
+ }
+ break;
+ default:
+ operation = OPERATION_INVALID;
+ break;
+ }
+
+ // Count request bytes.
+ if (stats->req_bytes) {
+ switch (operation) {
+ case OPERATION_QUERY:
+ knotd_mod_stats_incr(mod, tid, CTR_REQ_BYTES, REQ_BYTES_QUERY,
+ knot_pkt_size(qdata->query));
+ break;
+ case OPERATION_UPDATE:
+ knotd_mod_stats_incr(mod, tid, CTR_REQ_BYTES, REQ_BYTES_UPDATE,
+ knot_pkt_size(qdata->query));
+ break;
+ default:
+ if (xfr_packets <= 1) {
+ knotd_mod_stats_incr(mod, tid, CTR_REQ_BYTES, REQ_BYTES_OTHER,
+ knot_pkt_size(qdata->query));
+ }
+ break;
+ }
+ }
+
+ // Count response bytes.
+ if (stats->resp_bytes && state != KNOTD_STATE_NOOP) {
+ switch (operation) {
+ case OPERATION_QUERY:
+ knotd_mod_stats_incr(mod, tid, CTR_RESP_BYTES, RESP_BYTES_REPLY,
+ knot_pkt_size(pkt));
+ break;
+ case OPERATION_AXFR:
+ case OPERATION_IXFR:
+ knotd_mod_stats_incr(mod, tid, CTR_RESP_BYTES, RESP_BYTES_TRANSFER,
+ knot_pkt_size(pkt));
+ break;
+ default:
+ knotd_mod_stats_incr(mod, tid, CTR_RESP_BYTES, RESP_BYTES_OTHER,
+ knot_pkt_size(pkt));
+ break;
+ }
+ }
+
+ // Get the extended response code.
+ uint16_t rcode = qdata->rcode;
+ if (qdata->rcode_tsig != KNOT_RCODE_NOERROR) {
+ rcode = qdata->rcode_tsig;
+ }
+
+ // Count the response code.
+ if (stats->rcode && state != KNOTD_STATE_NOOP) {
+ if (xfr_packets <= 1 || rcode != KNOT_RCODE_NOERROR) {
+ if (xfr_packets > 1) {
+ assert(rcode != KNOT_RCODE_NOERROR);
+ // Ignore the leading XFR message NOERROR.
+ knotd_mod_stats_decr(mod, tid, CTR_RCODE,
+ KNOT_RCODE_NOERROR, 1);
+ }
+
+ if (qdata->rcode_tsig == KNOT_RCODE_BADSIG) {
+ knotd_mod_stats_incr(mod, tid, CTR_RCODE, RCODE_BADSIG, 1);
+ } else {
+ knotd_mod_stats_incr(mod, tid, CTR_RCODE,
+ MIN(rcode, RCODE_OTHER), 1);
+ }
+ }
+ }
+
+ // Return if non-first transfer message.
+ if (xfr_packets > 1) {
+ return state;
+ }
+
+ // Count the server operation.
+ if (stats->operation) {
+ knotd_mod_stats_incr(mod, tid, CTR_OPERATION, operation, 1);
+ }
+
+ // Count the request protocol.
+ if (stats->protocol) {
+ bool xdp = qdata->params->xdp_msg != NULL;
+ if (knotd_qdata_remote_addr(qdata)->ss_family == AF_INET) {
+ if (qdata->params->proto == KNOTD_QUERY_PROTO_UDP) {
+ if (xdp) {
+ knotd_mod_stats_incr(mod, tid, CTR_PROTOCOL,
+ PROTOCOL_UDP4_XDP, 1);
+ } else {
+ knotd_mod_stats_incr(mod, tid, CTR_PROTOCOL,
+ PROTOCOL_UDP4, 1);
+ }
+ } else if (qdata->params->proto == KNOTD_QUERY_PROTO_QUIC) {
+ if (xdp) {
+ knotd_mod_stats_incr(mod, tid, CTR_PROTOCOL,
+ PROTOCOL_QUIC4_XDP, 1);
+ } else {
+ knotd_mod_stats_incr(mod, tid, CTR_PROTOCOL,
+ PROTOCOL_QUIC4, 1);
+ }
+ } else {
+ if (xdp) {
+ knotd_mod_stats_incr(mod, tid, CTR_PROTOCOL,
+ PROTOCOL_TCP4_XDP, 1);
+ } else {
+ knotd_mod_stats_incr(mod, tid, CTR_PROTOCOL,
+ PROTOCOL_TCP4, 1);
+ }
+ }
+ } else {
+ if (qdata->params->proto == KNOTD_QUERY_PROTO_UDP) {
+ if (xdp) {
+ knotd_mod_stats_incr(mod, tid, CTR_PROTOCOL,
+ PROTOCOL_UDP6_XDP, 1);
+ } else {
+ knotd_mod_stats_incr(mod, tid, CTR_PROTOCOL,
+ PROTOCOL_UDP6, 1);
+ }
+ } else if (qdata->params->proto == KNOTD_QUERY_PROTO_QUIC) {
+ if (xdp) {
+ knotd_mod_stats_incr(mod, tid, CTR_PROTOCOL,
+ PROTOCOL_QUIC6_XDP, 1);
+ } else {
+ knotd_mod_stats_incr(mod, tid, CTR_PROTOCOL,
+ PROTOCOL_QUIC6, 1);
+ }
+ } else {
+ if (xdp) {
+ knotd_mod_stats_incr(mod, tid, CTR_PROTOCOL,
+ PROTOCOL_TCP6_XDP, 1);
+ } else {
+ knotd_mod_stats_incr(mod, tid, CTR_PROTOCOL,
+ PROTOCOL_TCP6, 1);
+ }
+ }
+ }
+ }
+
+ // Count EDNS occurrences.
+ if (stats->edns) {
+ if (knot_pkt_has_edns(qdata->query)) {
+ knotd_mod_stats_incr(mod, tid, CTR_EDNS, EDNS_REQ, 1);
+ }
+ if (knot_pkt_has_edns(pkt) && state != KNOTD_STATE_NOOP) {
+ knotd_mod_stats_incr(mod, tid, CTR_EDNS, EDNS_RESP, 1);
+ }
+ }
+
+ // Count interesting message header flags.
+ if (stats->flag) {
+ if (state != KNOTD_STATE_NOOP && knot_wire_get_tc(pkt->wire)) {
+ knotd_mod_stats_incr(mod, tid, CTR_FLAG, FLAG_TC, 1);
+ }
+ if (knot_pkt_has_dnssec(pkt)) {
+ knotd_mod_stats_incr(mod, tid, CTR_FLAG, FLAG_DO, 1);
+ }
+ }
+
+ // Count EDNS options.
+ if (stats->req_eopt) {
+ incr_edns_option(mod, tid, qdata->query, CTR_REQ_EOPT);
+ }
+ if (stats->resp_eopt) {
+ incr_edns_option(mod, tid, pkt, CTR_RESP_EOPT);
+ }
+
+ // Return if not query operation.
+ if (operation != OPERATION_QUERY) {
+ return state;
+ }
+
+ // Count NODATA reply (RFC 2308, Section 2.2).
+ if (stats->nodata && rcode == KNOT_RCODE_NOERROR && state != KNOTD_STATE_NOOP &&
+ knot_wire_get_ancount(pkt->wire) == 0 && !knot_wire_get_tc(pkt->wire) &&
+ (knot_wire_get_nscount(pkt->wire) == 0 ||
+ knot_pkt_rr(knot_pkt_section(pkt, KNOT_AUTHORITY), 0)->type == KNOT_RRTYPE_SOA)) {
+ switch (knot_pkt_qtype(qdata->query)) {
+ case KNOT_RRTYPE_A:
+ knotd_mod_stats_incr(mod, tid, CTR_NODATA, NODATA_A, 1);
+ break;
+ case KNOT_RRTYPE_AAAA:
+ knotd_mod_stats_incr(mod, tid, CTR_NODATA, NODATA_AAAA, 1);
+ break;
+ default:
+ knotd_mod_stats_incr(mod, tid, CTR_NODATA, NODATA_OTHER, 1);
+ break;
+ }
+ }
+
+ // Count the query type.
+ if (stats->qtype) {
+ uint16_t qtype = knot_pkt_qtype(qdata->query);
+
+ uint16_t idx;
+ switch (qtype) {
+ case QTYPE_MIN1 ... QTYPE_MAX1: idx = qtype; break;
+ case QTYPE_MIN2 ... QTYPE_MAX2: idx = qtype - QTYPE_SHIFT2; break;
+ case QTYPE_MIN3 ... QTYPE_MAX3: idx = qtype - QTYPE_SHIFT3; break;
+ default: idx = QTYPE_OTHER; break;
+ }
+
+ knotd_mod_stats_incr(mod, tid, CTR_QTYPE, idx, 1);
+ }
+
+ // Count the query size.
+ if (stats->qsize) {
+ uint64_t idx = knot_pkt_size(qdata->query) / BUCKET_SIZE;
+ knotd_mod_stats_incr(mod, tid, CTR_QSIZE, MIN(idx, QSIZE_MAX_IDX), 1);
+ }
+
+ // Count the reply size.
+ if (stats->rsize && state != KNOTD_STATE_NOOP) {
+ uint64_t idx = knot_pkt_size(pkt) / BUCKET_SIZE;
+ knotd_mod_stats_incr(mod, tid, CTR_RSIZE, MIN(idx, RSIZE_MAX_IDX), 1);
+ }
+
+ return state;
+}
+
+int stats_load(knotd_mod_t *mod)
+{
+ stats_t *stats = calloc(1, sizeof(*stats));
+ if (stats == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ for (const ctr_desc_t *desc = ctr_descs; desc->conf_name != NULL; desc++) {
+ knotd_conf_t conf = knotd_conf_mod(mod, desc->conf_name);
+ bool enabled = conf.single.boolean;
+
+ // Initialize corresponding configuration item.
+ *(bool *)((uint8_t *)stats + desc->conf_offset) = enabled;
+
+ int ret = knotd_mod_stats_add(mod, enabled ? desc->conf_name + 1 : NULL,
+ enabled ? desc->count : 1, desc->fcn);
+ if (ret != KNOT_EOK) {
+ free(stats);
+ return ret;
+ }
+ }
+
+ knotd_mod_ctx_set(mod, stats);
+
+ return knotd_mod_hook(mod, KNOTD_STAGE_END, update_counters);
+}
+
+void stats_unload(knotd_mod_t *mod)
+{
+ free(knotd_mod_ctx(mod));
+}
+
+KNOTD_MOD_API(stats, KNOTD_MOD_FLAG_SCOPE_ANY | KNOTD_MOD_FLAG_OPT_CONF,
+ stats_load, stats_unload, stats_conf, NULL);
diff --git a/src/knot/modules/stats/stats.rst b/src/knot/modules/stats/stats.rst
new file mode 100644
index 0000000..8acf1aa
--- /dev/null
+++ b/src/knot/modules/stats/stats.rst
@@ -0,0 +1,274 @@
+.. _mod-stats:
+
+``stats`` — Query statistics
+============================
+
+The module extends server statistics with incoming DNS request and corresponding
+response counters, such as used network protocol, total number of responded bytes,
+etc (see module reference for full list of supported counters).
+This module should be configured as the last module.
+
+.. NOTE::
+ Server initiated communication (outgoing NOTIFY, incoming \*XFR,...) is not
+ counted by this module.
+
+.. NOTE::
+ Leading 16-bit message size over TCP is not considered.
+
+Example
+-------
+
+Common statistics with default module configuration::
+
+ template:
+ - id: default
+ global-module: mod-stats
+
+Per zone statistics with explicit module configuration::
+
+ mod-stats:
+ - id: custom
+ edns-presence: on
+ query-type: on
+
+ template:
+ - id: default
+ module: mod-stats/custom
+
+Module reference
+----------------
+
+::
+
+ mod-stats:
+ - id: STR
+ request-protocol: BOOL
+ server-operation: BOOL
+ request-bytes: BOOL
+ response-bytes: BOOL
+ edns-presence: BOOL
+ flag-presence: BOOL
+ response-code: BOOL
+ request-edns-option: BOOL
+ response-edns-option: BOOL
+ reply-nodata: BOOL
+ query-type: BOOL
+ query-size: BOOL
+ reply-size: BOOL
+
+.. _mod-stats_id:
+
+id
+..
+
+A module identifier.
+
+.. _mod-stats_request-protocol:
+
+request-protocol
+................
+
+If enabled, all incoming requests are counted by the network protocol:
+
+* udp4 - UDP over IPv4
+* tcp4 - TCP over IPv4
+* quic4 - QUIC over IPv4
+* udp6 - UDP over IPv6
+* tcp6 - TCP over IPv6
+* quic6 - QUIC over IPv6
+* udp4-xdp - UDP over IPv4 through XDP
+* tcp4-xdp - TCP over IPv4 through XDP
+* quic4-xdp - QUIC over IPv4 through XDP
+* udp6-xdp - UDP over IPv6 through XDP
+* tcp6-xdp - TCP over IPv6 through XDP
+* quic6-xdp - QUIC over IPv6 through XDP
+
+*Default:* ``on``
+
+.. _mod-stats_server-operation:
+
+server-operation
+................
+
+If enabled, all incoming requests are counted by the server operation. The
+server operation is based on message header OpCode and message query (meta) type:
+
+* query - Normal query operation
+* update - Dynamic update operation
+* notify - NOTIFY request operation
+* axfr - Full zone transfer operation
+* ixfr - Incremental zone transfer operation
+* invalid - Invalid server operation
+
+*Default:* ``on``
+
+.. _mod-stats_request-bytes:
+
+request-bytes
+.............
+
+If enabled, all incoming request bytes are counted by the server operation:
+
+* query - Normal query bytes
+* update - Dynamic update bytes
+* other - Other request bytes
+
+*Default:* ``on``
+
+.. _mod-stats_response-bytes:
+
+response-bytes
+..............
+
+If enabled, outgoing response bytes are counted by the server operation:
+
+* reply - Normal response bytes
+* transfer - Zone transfer bytes
+* other - Other response bytes
+
+.. WARNING::
+ Dynamic update response bytes are not counted by this module.
+
+*Default:* ``on``
+
+.. _mod-stats_edns-presence:
+
+edns-presence
+.............
+
+If enabled, EDNS pseudo section presence is counted by the message direction:
+
+* request - EDNS present in request
+* response - EDNS present in response
+
+*Default:* ``off``
+
+.. _mod-stats_flag-presence:
+
+flag-presence
+.............
+
+If enabled, some message header flags are counted:
+
+* TC - Truncated Answer in response
+* DO - DNSSEC OK in request
+
+*Default:* ``off``
+
+.. _mod-stats_response-code:
+
+response-code
+.............
+
+If enabled, outgoing response code is counted:
+
+* NOERROR
+* ...
+* NOTZONE
+* BADVERS
+* ...
+* BADCOOKIE
+* other - All other codes
+
+.. NOTE::
+ In the case of multi-message zone transfer response, just one counter is
+ incremented.
+
+.. WARNING::
+ Dynamic update response code is not counted by this module.
+
+*Default:* ``on``
+
+.. _mod-stats_request-edns-option:
+
+request-edns-option
+...................
+
+If enabled, EDNS options in requests are counted by their code:
+
+* CODE0
+* ...
+* EDNS-KEY-TAG (CODE14)
+* other - All other codes
+
+*Default:* ``off``
+
+.. _mod-stats_response-edns-option:
+
+response-edns-option
+....................
+
+If enabled, EDNS options in responses are counted by their code. See
+:ref:`mod-stats_request-edns-option`.
+
+*Default:* ``off``
+
+.. _mod-stats_reply-nodata:
+
+reply-nodata
+............
+
+If enabled, NODATA pseudo RCODE (:rfc:`2308#section-2.2`) is counted by the
+query type:
+
+* A
+* AAAA
+* other - All other types
+
+*Default:* ``off``
+
+.. _mod-stats_query-type:
+
+query-type
+..........
+
+If enabled, normal query type is counted:
+
+* A (TYPE1)
+* ...
+* TYPE65
+* SPF (TYPE99)
+* ...
+* TYPE110
+* ANY (TYPE255)
+* ...
+* TYPE260
+* other - All other types
+
+.. NOTE::
+ Not all assigned meta types (IXFR, AXFR,...) have their own counters,
+ because such types are not processed as normal query.
+
+*Default:* ``off``
+
+.. _mod-stats_query-size:
+
+query-size
+..........
+
+If enabled, normal query message size distribution is counted by the size range
+in bytes:
+
+* 0-15
+* 16-31
+* ...
+* 272-287
+* 288-65535
+
+*Default:* ``off``
+
+.. _mod-stats_reply-size:
+
+reply-size
+..........
+
+If enabled, normal reply message size distribution is counted by the size range
+in bytes:
+
+* 0-15
+* 16-31
+* ...
+* 4080-4095
+* 4096-65535
+
+*Default:* ``off``