]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
dnsdist: Add a 'rings' endpoint to the REST API 13489/head
authorRemi Gacogne <remi.gacogne@powerdns.com>
Mon, 13 Nov 2023 16:36:11 +0000 (17:36 +0100)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Tue, 14 Nov 2023 13:03:08 +0000 (14:03 +0100)
pdns/dnsdist-web.cc
pdns/dnsdistdist/dnsdist-lua-ffi.cc
pdns/dnsdistdist/docs/guides/webserver.rst
pdns/dnsdistdist/test-dnsdist-lua-ffi.cc
regression-tests.dnsdist/test_API.py

index c4cf702f504c7d9ca04b0bbd3f45423e90b631f2..d0890b5462de8bd3b6db961db71638e29c533be2 100644 (file)
@@ -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<typename T> static void addRingEntryToList(const struct timespec& now, Json::array& list, const T& entry)
+{
+  constexpr bool response = std::is_same_v<T, Rings::Response>;
+  Json::object tmp{
+    { "age", static_cast<double>(DiffTime(entry.when, now)) },
+    { "id", ntohs(entry.dh.id) },
+    { "name", entry.name.toString() },
+    { "requestor", entry.requestor.toStringWithPort() },
+    { "size", static_cast<int>(entry.size) },
+    { "qtype", entry.qtype },
+    { "protocol", entry.protocol.toString() },
+    { "rd", static_cast<bool>(entry.dh.rd) },
+  };
+  if constexpr (!response) {
+#if defined(DNSDIST_RINGS_WITH_MACADDRESS)
+    tmp.emplace("mac", entry.hasmac ? std::string(reinterpret_cast<const char*>(entry.macaddress.data()), entry.macaddress.size()) : std::string());
+#endif
+  }
+  else {
+    tmp.emplace("latency", static_cast<double>(entry.usec));
+    tmp.emplace("rcode", static_cast<uint8_t>(entry.dh.rcode));
+    tmp.emplace("tc", static_cast<bool>(entry.dh.tc));
+    tmp.emplace("aa", static_cast<bool>(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<size_t> maxNumberOfQueries{std::nullopt};
+  std::optional<size_t> maxNumberOfResponses{std::nullopt};
+
+  const auto maxQueries = req.getvars.find("maxQueries");
+  if (maxQueries != req.getvars.end()) {
+    try {
+      maxNumberOfQueries = pdns::checked_stoi<size_t>(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<size_t>(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<std::string, std::function<void(const YaHTTP::Request&, YaHTTP::Response&)>> s_webHandlers;
 
 void registerWebHandler(const std::string& endpoint, std::function<void(const YaHTTP::Request&, YaHTTP::Response&)> 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);
index afc53f4faf5d439cb4a636496503058520bf08a3..f7365bf7718b2c885072e767e93ac41091d39ea2 100644 (file)
@@ -1317,15 +1317,15 @@ template<typename T> static void addRingEntryToList(std::unique_ptr<dnsdist_ffi_
   constexpr bool response = std::is_same_v<T, Rings::Response>;
 #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<const char*>(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<const char*>(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
 }
index 2ad6c449568c8527b61e64f550c5d9103afb14b5..96cab46b8868385597809d149f1e81d61167ad9f 100755 (executable)
@@ -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
index b886b1fd49b3c4b82ec9c710076d3e556c864215..56047a7f0c8e79ae4c5da787f34d42cf3a8b1b0f 100644 (file)
@@ -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));
index 322b63bbd5fa876aec2e2d00dc209db9219396bc..2e704550877f3fc908e5b5ffa751838b8a2c88fd 100644 (file)
@@ -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 = """