From: Remi Gacogne Date: Mon, 13 Nov 2023 16:36:11 +0000 (+0100) Subject: dnsdist: Add a 'rings' endpoint to the REST API X-Git-Tag: rec-5.0.0-rc1~43^2 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=d0538b8486f66efea0630dd3e00b01ae968fdea0;p=thirdparty%2Fpdns.git dnsdist: Add a 'rings' endpoint to the REST API --- diff --git a/pdns/dnsdist-web.cc b/pdns/dnsdist-web.cc index c4cf702f50..d0890b5462 100644 --- a/pdns/dnsdist-web.cc +++ b/pdns/dnsdist-web.cc @@ -36,6 +36,7 @@ #include "dnsdist-healthchecks.hh" #include "dnsdist-metrics.hh" #include "dnsdist-prometheus.hh" +#include "dnsdist-rings.hh" #include "dnsdist-web.hh" #include "dolog.hh" #include "gettime.hh" @@ -1629,6 +1630,104 @@ static void handleCacheManagement(const YaHTTP::Request& req, YaHTTP::Response& } #endif /* DISABLE_WEB_CACHE_MANAGEMENT */ +template static void addRingEntryToList(const struct timespec& now, Json::array& list, const T& entry) +{ + constexpr bool response = std::is_same_v; + Json::object tmp{ + { "age", static_cast(DiffTime(entry.when, now)) }, + { "id", ntohs(entry.dh.id) }, + { "name", entry.name.toString() }, + { "requestor", entry.requestor.toStringWithPort() }, + { "size", static_cast(entry.size) }, + { "qtype", entry.qtype }, + { "protocol", entry.protocol.toString() }, + { "rd", static_cast(entry.dh.rd) }, + }; + if constexpr (!response) { +#if defined(DNSDIST_RINGS_WITH_MACADDRESS) + tmp.emplace("mac", entry.hasmac ? std::string(reinterpret_cast(entry.macaddress.data()), entry.macaddress.size()) : std::string()); +#endif + } + else { + tmp.emplace("latency", static_cast(entry.usec)); + tmp.emplace("rcode", static_cast(entry.dh.rcode)); + tmp.emplace("tc", static_cast(entry.dh.tc)); + tmp.emplace("aa", static_cast(entry.dh.aa)); + tmp.emplace("answers", ntohs(entry.dh.ancount)); + auto server = entry.ds.toStringWithPort(); + tmp.emplace("backend", server != "0.0.0.0:0" ? std::move(server) : "Cache"); + } + list.push_back(std::move(tmp)); +} + +static void handleRings(const YaHTTP::Request& req, YaHTTP::Response& resp) +{ + handleCORS(req, resp); + + std::optional maxNumberOfQueries{std::nullopt}; + std::optional maxNumberOfResponses{std::nullopt}; + + const auto maxQueries = req.getvars.find("maxQueries"); + if (maxQueries != req.getvars.end()) { + try { + maxNumberOfQueries = pdns::checked_stoi(maxQueries->second); + } + catch (const std::exception& exp) { + vinfolog("Error parsing the 'maxQueries' value from rings HTTP GET query: %s", exp.what()); + } + } + + const auto maxResponses = req.getvars.find("maxResponses"); + if (maxResponses != req.getvars.end()) { + try { + maxNumberOfResponses = pdns::checked_stoi(maxResponses->second); + } + catch (const std::exception& exp) { + vinfolog("Error parsing the 'maxResponses' value from rings HTTP GET query: %s", exp.what()); + } + } + + resp.status = 200; + + Json::object doc; + size_t numberOfQueries = 0; + size_t numberOfResponses = 0; + Json::array queries; + Json::array responses; + struct timespec now + { + }; + gettime(&now); + + for (const auto& shard : g_rings.d_shards) { + if (!maxNumberOfQueries || numberOfQueries < *maxNumberOfQueries) { + auto queryRing = shard->queryRing.lock(); + for (const auto& entry : *queryRing) { + addRingEntryToList(now, queries, entry); + numberOfQueries++; + if (maxNumberOfQueries && numberOfQueries >= *maxNumberOfQueries) { + break; + } + } + } + if (!maxNumberOfResponses || numberOfResponses < *maxNumberOfResponses) { + auto responseRing = shard->respRing.lock(); + for (const auto& entry : *responseRing) { + addRingEntryToList(now, responses, entry); + numberOfResponses++; + if (maxNumberOfResponses && numberOfResponses >= *maxNumberOfResponses) { + break; + } + } + } + } + doc.emplace("queries", std::move(queries)); + doc.emplace("responses", std::move(responses)); + Json my_json = doc; + resp.body = my_json.dump(); + resp.headers["Content-Type"] = "application/json"; +} + static std::unordered_map> s_webHandlers; void registerWebHandler(const std::string& endpoint, std::function handler); @@ -1693,6 +1792,7 @@ void registerBuiltInWebHandlers() registerWebHandler("/api/v1/servers/localhost", handleStats); registerWebHandler("/api/v1/servers/localhost/pool", handlePoolStats); registerWebHandler("/api/v1/servers/localhost/statistics", handleStatsOnly); + registerWebHandler("/api/v1/servers/localhost/rings", handleRings); #ifndef DISABLE_WEB_CONFIG registerWebHandler("/api/v1/servers/localhost/config", handleConfigDump); registerWebHandler("/api/v1/servers/localhost/config/allow-from", handleAllowFrom); diff --git a/pdns/dnsdistdist/dnsdist-lua-ffi.cc b/pdns/dnsdistdist/dnsdist-lua-ffi.cc index afc53f4faf..f7365bf771 100644 --- a/pdns/dnsdistdist/dnsdist-lua-ffi.cc +++ b/pdns/dnsdistdist/dnsdist-lua-ffi.cc @@ -1317,15 +1317,15 @@ template static void addRingEntryToList(std::unique_ptr; #if defined(DNSDIST_RINGS_WITH_MACADDRESS) if constexpr (!response) { - dnsdist_ffi_ring_entry_list_t::entry tmp{entry.name.toString(), entry.requestor.toString(), entry.hasmac ? std::string(reinterpret_cast(entry.macaddress.data()), entry.macaddress.size()) : std::string(), entry.size, entry.qtype, entry.protocol, response}; + dnsdist_ffi_ring_entry_list_t::entry tmp{entry.name.toString(), entry.requestor.toStringWithPort(), entry.hasmac ? std::string(reinterpret_cast(entry.macaddress.data()), entry.macaddress.size()) : std::string(), entry.size, entry.qtype, entry.protocol, response}; list->d_entries.push_back(std::move(tmp)); } else { - dnsdist_ffi_ring_entry_list_t::entry tmp{entry.name.toString(), entry.requestor.toString(), std::string(), entry.size, entry.qtype, entry.protocol, response}; + dnsdist_ffi_ring_entry_list_t::entry tmp{entry.name.toString(), entry.requestor.toStringWithPort(), std::string(), entry.size, entry.qtype, entry.protocol, response}; list->d_entries.push_back(std::move(tmp)); } #else - dnsdist_ffi_ring_entry_list_t::entry tmp{entry.name.toString(), entry.requestor.toString(), std::string(), entry.size, entry.qtype, entry.protocol, response}; + dnsdist_ffi_ring_entry_list_t::entry tmp{entry.name.toString(), entry.requestor.toStringWithPort(), std::string(), entry.size, entry.qtype, entry.protocol, response}; list->d_entries.push_back(std::move(tmp)); #endif } diff --git a/pdns/dnsdistdist/docs/guides/webserver.rst b/pdns/dnsdistdist/docs/guides/webserver.rst index 2ad6c44956..96cab46b88 100755 --- a/pdns/dnsdistdist/docs/guides/webserver.rst +++ b/pdns/dnsdistdist/docs/guides/webserver.rst @@ -793,6 +793,16 @@ URL Endpoints :>json list: A list of metrics related to that pool :>json list servers: A list of :json:object:`Server` objects present in that pool +.. http:get:: /api/v1/servers/localhost/rings?maxQueries=NUM&maxResponses=NUM + + .. versionadded:: 1.9.0 + + Get the most recent queries and responses from the in-memory ring buffers. Returns up to ``maxQueries`` + query entries if set, up to ``maxResponses`` responses if set, and the whole content of the ring buffers otherwise. + + :>json list queries: The list of the most recent queries, as :json:object:`RingEntry` objects + :>json list responses: The list of the most recent responses, as :json:object:`RingEntry` objects + JSON Objects ~~~~~~~~~~~~ @@ -969,3 +979,23 @@ JSON Objects :property string name: The name of this statistic. See :doc:`../statistics` :property string type: "StatisticItem" :property integer value: The value for this item + +.. json:object:: RingEntry + + This represents an entry in the in-memory ring buffers. + + :property float age: How long ago was the query or response received, in seconds + :property integer id: The DNS ID + :property string name: The requested domain name + :property string requestor: The client IP and port + :property integer size: The size of the query or response + :property integer qtype: The requested DNS type + :property string protocol: The DNS protocol the query or response was received over + :property boolean rd: The RD flag + :property string mac: The MAC address of the device sending the query + :property float latency: The time it took for the response to be sent back to the client, in microseconds + :property int rcode: The response code + :property boolean tc: The TC flag + :property boolean aa: The AA flag + :property integer answers: The number of records in the answer section of the response + :property string backend: The IP and port of the backend that returned the response, or "Cache" if it was a cache-hit diff --git a/pdns/dnsdistdist/test-dnsdist-lua-ffi.cc b/pdns/dnsdistdist/test-dnsdist-lua-ffi.cc index b886b1fd49..56047a7f0c 100644 --- a/pdns/dnsdistdist/test-dnsdist-lua-ffi.cc +++ b/pdns/dnsdistdist/test-dnsdist-lua-ffi.cc @@ -734,7 +734,7 @@ BOOST_AUTO_TEST_CASE(test_RingBuffers) for (size_t idx = 0; idx < 2; idx++) { BOOST_CHECK(dnsdist_ffi_ring_entry_get_name(list, idx) == qname.toString()); BOOST_CHECK(dnsdist_ffi_ring_entry_get_type(list, idx) == qtype); - BOOST_CHECK(dnsdist_ffi_ring_entry_get_requestor(list, idx) == requestor1.toString()); + BOOST_CHECK(dnsdist_ffi_ring_entry_get_requestor(list, idx) == requestor1.toStringWithPort()); BOOST_CHECK(dnsdist_ffi_ring_entry_get_protocol(list, idx) == protocol.toNumber()); BOOST_CHECK_EQUAL(dnsdist_ffi_ring_entry_get_size(list, idx), size); BOOST_CHECK(!dnsdist_ffi_ring_entry_has_mac_address(list, idx)); diff --git a/regression-tests.dnsdist/test_API.py b/regression-tests.dnsdist/test_API.py index 322b63bbd5..2e70455087 100644 --- a/regression-tests.dnsdist/test_API.py +++ b/regression-tests.dnsdist/test_API.py @@ -2,6 +2,7 @@ import os.path import base64 +import dns import json import requests import socket @@ -351,6 +352,59 @@ class TestAPIBasics(APITestsBase): for key in ['blocks']: self.assertTrue(content[key] >= 0) + def testServersLocalhostRings(self): + """ + API: /api/v1/servers/localhost/rings + """ + headers = {'x-api-key': self._webServerAPIKey} + url = 'http://127.0.0.1:' + str(self._webServerPort) + '/api/v1/servers/localhost/rings' + expectedValues = ['age', 'id', 'name', 'requestor', 'size', 'qtype', 'protocol', 'rd'] + expectedResponseValues = expectedValues + ['latency', 'rcode', 'tc', 'aa', 'answers', 'backend'] + r = requests.get(url, headers=headers, timeout=self._webTimeout) + self.assertTrue(r) + self.assertEqual(r.status_code, 200) + self.assertTrue(r.json()) + content = r.json() + self.assertIn('queries', content) + self.assertIn('responses', content) + self.assertEqual(len(content['queries']), 0) + self.assertEqual(len(content['responses']), 0) + + name = 'simple.api.tests.powerdns.com.' + query = dns.message.make_query(name, 'A', 'IN') + response = dns.message.make_response(query) + rrset = dns.rrset.from_text(name, + 3600, + dns.rdataclass.IN, + dns.rdatatype.A, + '127.0.0.1') + response.answer.append(rrset) + + for method in ("sendUDPQuery", "sendTCPQuery"): + sender = getattr(self, method) + (receivedQuery, receivedResponse) = sender(query, response) + self.assertTrue(receivedQuery) + self.assertTrue(receivedResponse) + receivedQuery.id = query.id + self.assertEqual(query, receivedQuery) + self.assertEqual(response, receivedResponse) + + r = requests.get(url, headers=headers, timeout=self._webTimeout) + self.assertTrue(r) + self.assertEqual(r.status_code, 200) + self.assertTrue(r.json()) + content = r.json() + self.assertIn('queries', content) + self.assertIn('responses', content) + self.assertEqual(len(content['queries']), 2) + self.assertEqual(len(content['responses']), 2) + for entry in content['queries']: + for value in expectedValues: + self.assertIn(value, entry) + for entry in content['responses']: + for value in expectedResponseValues: + self.assertIn(value, entry) + class TestAPIServerDown(APITestsBase): __test__ = True _config_template = """