]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
Add RTT statistics tests both for XML and JSON outputs
authorAram Sargsyan <aram@isc.org>
Fri, 16 Jan 2026 14:02:54 +0000 (14:02 +0000)
committerArаm Sаrgsyаn <aram@isc.org>
Thu, 26 Feb 2026 14:00:10 +0000 (14:00 +0000)
Add a resolver instance "ns4" in the statschannel test and a "ans5"
instance which adds latency to the queries delegeated to it from the
resolver.

Make queries which add latency, and compare the expected values to
the values received from the statistics channel.

bin/tests/system/statschannel/ans5/ans.py [new file with mode: 0644]
bin/tests/system/statschannel/generic.py
bin/tests/system/statschannel/ns4/example2.db [new file with mode: 0644]
bin/tests/system/statschannel/ns4/named.conf.j2 [new file with mode: 0644]
bin/tests/system/statschannel/tests_json.py
bin/tests/system/statschannel/tests_sh_statschannel.py
bin/tests/system/statschannel/tests_xml.py

diff --git a/bin/tests/system/statschannel/ans5/ans.py b/bin/tests/system/statschannel/ans5/ans.py
new file mode 100644 (file)
index 0000000..6f7dcc2
--- /dev/null
@@ -0,0 +1,56 @@
+"""
+Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+
+SPDX-License-Identifier: MPL-2.0
+
+This Source Code Form is subject to the terms of the Mozilla Public
+License, v. 2.0.  If a copy of the MPL was not distributed with this
+file, you can obtain one at https://mozilla.org/MPL/2.0/.
+
+See the COPYRIGHT file distributed with this work for additional
+information regarding copyright ownership.
+"""
+
+from collections.abc import AsyncGenerator
+
+import dns.rcode
+import dns.rdatatype
+import dns.rrset
+
+from isctest.asyncserver import (
+    ControllableAsyncDnsServer,
+    DnsResponseSend,
+    QueryContext,
+    ResponseHandler,
+)
+
+
+class DelayedAddressAnswerHandler(ResponseHandler):
+    async def get_responses(
+        self, qctx: QueryContext
+    ) -> AsyncGenerator[DnsResponseSend, None]:
+        if qctx.qtype in (dns.rdatatype.A, dns.rdatatype.AAAA):
+            addr = "192.0.2.1" if qctx.qtype == dns.rdatatype.A else "2001:db8:beef::1"
+            rrset = dns.rrset.from_text(qctx.qname, 300, qctx.qclass, qctx.qtype, addr)
+            qctx.response.answer.append(rrset)
+
+        delay = 0
+        if (
+            len(qctx.qname.labels) >= 2
+            and qctx.qname.labels[1] == b"latency"
+            and qctx.qname.labels[0].isdigit()
+        ):
+            delay = int(qctx.qname.labels[0]) / 1000
+        yield DnsResponseSend(qctx.response, delay=delay)
+
+
+def main() -> None:
+    server = ControllableAsyncDnsServer(
+        default_aa=True, default_rcode=dns.rcode.NOERROR
+    )
+    server.install_response_handler(DelayedAddressAnswerHandler())
+    server.run()
+
+
+if __name__ == "__main__":
+    main()
index 0b0ee82de05da70952c167929021f8e6a3d81cf1..02a7eea38306b3e663a4dc2d4ed6e843a6251d36 100644 (file)
@@ -58,6 +58,11 @@ def check_zone_timers(loaded, expires, refresh, loaded_exp):
     check_loaded(loaded, loaded_exp, now)
 
 
+def check_rtt(rtt, rtt_expected):
+    for val in rtt_expected:
+        assert rtt[val[0]] == val[1]
+
+
 #
 # The output is gibberish, but at least make sure it does not crash.
 #
@@ -225,3 +230,33 @@ def test_traffic(fetch_traffic, **kwargs):
     data = fetch_traffic(statsip, statsport)
 
     check_traffic(data, exp)
+
+
+def test_rtt(fetch_views, **kwargs):
+    statsip = kwargs["statsip"]
+    statsport = kwargs["statsport"]
+
+    # auth query, 0 delay is expected, only for "in"
+    msg = create_msg("a.example2.", "TXT")
+    ans = isctest.query.tcp(msg, statsip, attempts=1)
+    isctest.check.noerror(ans)
+
+    # resolver query with a 530ms delay for both "in" and "out"
+    msg = create_msg("530.latency.example2.", "A")
+    ans = isctest.query.tcp(msg, statsip, attempts=1)
+    isctest.check.noerror(ans)
+
+    # resolver query with a 540ms delay for both "in" and "out"
+    msg = create_msg("540.latency.example2.", "A")
+    ans = isctest.query.tcp(msg, statsip, attempts=1)
+    isctest.check.noerror(ans)
+
+    # resolver query with a 730ms delay for both "in" and "out"
+    msg = create_msg("730.latency.example2.", "A")
+    ans = isctest.query.tcp(msg, statsip, attempts=1)
+    isctest.check.noerror(ans)
+
+    data = fetch_views(statsip, statsport)
+
+    check_rtt(data["in-queries-rtt"], [["~0", 1], ["512-575", 2], ["704-767", 1]])
+    check_rtt(data["out-queries-rtt"], [["512-575", 2], ["704-767", 1]])
diff --git a/bin/tests/system/statschannel/ns4/example2.db b/bin/tests/system/statschannel/ns4/example2.db
new file mode 100644 (file)
index 0000000..2d623eb
--- /dev/null
@@ -0,0 +1,28 @@
+; Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+;
+; SPDX-License-Identifier: MPL-2.0
+;
+; This Source Code Form is subject to the terms of the Mozilla Public
+; License, v. 2.0.  If a copy of the MPL was not distributed with this
+; file, you can obtain one at https://mozilla.org/MPL/2.0/.
+;
+; See the COPYRIGHT file distributed with this work for additional
+; information regarding copyright ownership.
+
+$ORIGIN .
+$TTL 300       ; 5 minutes
+example2                       IN SOA  mname1. . (
+                               1          ; serial
+                               20         ; refresh (20 seconds)
+                               20         ; retry (20 seconds)
+                               1814400    ; expire (3 weeks)
+                               3600       ; minimum (1 hour)
+                               )
+example2.              NS      ns4.example2.
+ns4.example2.          A       10.53.0.4
+
+$ORIGIN example2.
+a                      A       10.0.0.1
+
+latency                        NS      ns5.example2.
+ns5.example2.          A       10.53.0.5
diff --git a/bin/tests/system/statschannel/ns4/named.conf.j2 b/bin/tests/system/statschannel/ns4/named.conf.j2
new file mode 100644 (file)
index 0000000..4e2c17e
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+ *
+ * SPDX-License-Identifier: MPL-2.0
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0.  If a copy of the MPL was not distributed with this
+ * file, you can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * See the COPYRIGHT file distributed with this work for additional
+ * information regarding copyright ownership.
+ */
+
+options {
+       query-source address 10.53.0.4;
+       notify-source 10.53.0.4;
+       transfer-source 10.53.0.4;
+       port @PORT@;
+       pid-file "named.pid";
+       listen-on { 10.53.0.4; };
+       listen-on-v6 { none; };
+       recursion yes;
+       dnssec-validation no;
+       notify no;
+       minimal-responses no;
+       version none;  // make statistics independent of the version number
+};
+
+statistics-channels { inet 10.53.0.4 port @EXTRAPORT1@ allow { localhost; }; };
+
+key rndc_key {
+       secret "1234abcd8765";
+       algorithm @DEFAULT_HMAC@;
+};
+
+controls {
+       inet 10.53.0.4 port @CONTROLPORT@ allow { any; } keys { rndc_key; };
+};
+
+zone "example2" {
+       type primary;
+       file "example2.db";
+       allow-transfer { any; };
+};
index 30348f4e69f569ae91d6b92815316a8d6c47be7b..f1da9e9c82660778b9e5510b2375e637dc7817c2 100755 (executable)
@@ -24,6 +24,7 @@ pytestmark = [
     isctest.mark.with_json_c,
     pytest.mark.extra_artifacts(
         [
+            "ans5/ans.run",
             "ns2/*.jnl",
             "ns2/*.signed",
             "ns2/dsset-*",
@@ -62,6 +63,19 @@ def fetch_traffic_json(statsip, statsport):
     return data["traffic"]
 
 
+def fetch_rtt_json(statsip, statsport):
+    r = requests.get(f"http://{statsip}:{statsport}/json/v1", timeout=600)
+    assert r.status_code == 200
+
+    views = r.json()["views"]
+    data = {
+        "in-queries-rtt": views["_default"]["resolver"]["in-queries-rtt"],
+        "out-queries-rtt": views["_default"]["resolver"]["out-queries-rtt"],
+    }
+
+    return data
+
+
 def load_timers_json(zone, primary=True):
     name = zone["name"]
 
@@ -119,3 +133,8 @@ def test_zone_with_many_keys_json(statsport):
 @pytest.mark.flaky(max_runs=2)
 def test_traffic_json(statsport):
     generic.test_traffic(fetch_traffic_json, statsip="10.53.0.2", statsport=statsport)
+
+
+@pytest.mark.flaky(max_runs=2)
+def test_rtt_json(statsport):
+    generic.test_rtt(fetch_rtt_json, statsip="10.53.0.4", statsport=statsport)
index 7b5788010d6593404005455e37ac22b7c16de167..473646cf1b3551e98e1e2e5acb0f5e5aaf26e2e0 100644 (file)
@@ -14,6 +14,7 @@ import pytest
 pytestmark = pytest.mark.extra_artifacts(
     [
         "K*",
+        "ans5/ans.run",
         "bind9.xsl.1",
         "bind9.xsl.2",
         "compressed.headers",
index 0707681de7f69c38896fa1e25e4745abe07e6ede..133c2f09633c084da66cd9b6ece8edcf87c86056 100755 (executable)
@@ -27,6 +27,7 @@ pytestmark = [
     pytest.mark.extra_artifacts(
         [
             "ns2/K*",
+            "ans5/ans.run",
             "ns2/*.jnl",
             "ns2/*.signed",
             "ns2/dsset-*",
@@ -91,6 +92,35 @@ def fetch_traffic_xml(statsip, statsport):
     return traffic
 
 
+def fetch_rtt_xml(statsip, statsport):
+    def load_counters(data):
+        out = {}
+        for counter in data.findall("counter"):
+            out[counter.attrib["name"]] = int(counter.text)
+
+        return out
+
+    r = requests.get(f"http://{statsip}:{statsport}/xml/v3", timeout=600)
+    assert r.status_code == 200
+
+    root = ET.fromstring(r.text)
+
+    default_view = None
+    for view in root.find("views").iter("view"):
+        if view.attrib["name"] == "_default":
+            default_view = view
+            break
+    assert default_view is not None
+
+    rtt = {}
+    for counters in default_view.find("rtt").findall("counters"):
+        key = counters.attrib["type"]
+        values = load_counters(counters)
+        rtt[key] = values
+
+    return rtt
+
+
 def load_timers_xml(zone, primary=True):
     name = zone.attrib["name"]
 
@@ -149,3 +179,8 @@ def test_zone_with_many_keys_xml(statsport):
 @pytest.mark.flaky(max_runs=2)
 def test_traffic_xml(statsport):
     generic.test_traffic(fetch_traffic_xml, statsip="10.53.0.2", statsport=statsport)
+
+
+@pytest.mark.flaky(max_runs=2)
+def test_rtt_xml(statsport):
+    generic.test_rtt(fetch_rtt_xml, statsip="10.53.0.4", statsport=statsport)