#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"
}
#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);
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);
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
}
:>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
~~~~~~~~~~~~
: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
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));
import os.path
import base64
+import dns
import json
import requests
import socket
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 = """