]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
dnsdist: add support for user defined metrics
authorCharles-Henri Bruyand <charles-henri.bruyand@open-xchange.com>
Wed, 1 Jun 2022 08:01:16 +0000 (10:01 +0200)
committerCharles-Henri Bruyand <charles-henri.bruyand@open-xchange.com>
Wed, 1 Jun 2022 14:43:01 +0000 (16:43 +0200)
pdns/dnsdist-carbon.cc
pdns/dnsdist-lua.cc
pdns/dnsdist-web.cc
pdns/dnsdist.hh
pdns/dnsdistdist/docs/reference/custommetrics.rst [new file with mode: 0644]
pdns/dnsdistdist/docs/reference/index.rst
regression-tests.dnsdist/test_API.py
regression-tests.dnsdist/test_Advanced.py

index cdad9dd69a74b69abbb624b1d3d722fa4c040ad4..b405c13b11ecd9cf48fd303505c6a3a491011db9 100644 (file)
@@ -75,6 +75,8 @@ void carbonDumpThread()
             str<<namespace_name<<"."<<hostname<<"."<<instance_name<<"."<<e.first<<' ';
             if(const auto& val = boost::get<pdns::stat_t*>(&e.second))
               str<<(*val)->load();
+            else if(const auto& adval = boost::get<pdns::stat_t_trait<double>*>(&e.second))
+              str<<(*adval)->load();
             else if (const auto& dval = boost::get<double*>(&e.second))
               str<<**dval;
             else
index a2f2fac7c6d8484b19480a66836c2a8144805db0..7df6abe007d6823c8c7c10fcc25acdc859caccd9 100644 (file)
@@ -29,6 +29,7 @@
 #include <sys/socket.h>
 #include <net/if.h>
 
+#include <regex>
 #include <sys/types.h>
 #include <sys/stat.h>
 #include <thread>
@@ -2861,6 +2862,67 @@ static void setupLuaConfig(LuaContext& luaCtx, bool client, bool configCheck)
     std::thread newThread(LuaThread, code);
     newThread.detach();
   });
+
+  luaCtx.writeFunction("declareMetric", [](const std::string& name, const std::string& type) {
+    if (g_configurationDone) {
+      g_outputBuffer = "declareMetric cannot be used at runtime!\n";
+      return false;
+    }
+    if (!std::regex_match(name, std::regex("^[a-z0-9-]+$"))) {
+      return false;
+    }
+    if (type == "counter") {
+      auto itp = g_stats.customCounters.emplace(name, 0);
+      if (itp.second) {
+        g_stats.entries.emplace_back(name, &g_stats.customCounters[name]);
+      }
+    } else if (type == "gauge") {
+      auto itp = g_stats.customGauges.emplace(name, 0.);
+      if (itp.second) {
+        g_stats.entries.emplace_back(name, &g_stats.customGauges[name]);
+      }
+    } else {
+      g_outputBuffer = "declareMetric unknown type '" + type + "'\n";
+      errlog("Unable to declareMetric '%s': no such type '%s'", name, type);
+      return false;
+    }
+    return true;
+  });
+  luaCtx.writeFunction("incMetric", [](const std::string& name) {
+    if (g_stats.customCounters.count(name) > 0) {
+      return ++g_stats.customCounters[name];
+    }
+    g_outputBuffer = "incMetric no such metric '" + name + "'\n";
+    errlog("Unable to incMetric: no such name '%s'", name);
+    return (uint64_t)0;
+  });
+  luaCtx.writeFunction("decMetric", [](const std::string& name) {
+    if (g_stats.customCounters.count(name) > 0) {
+      return --g_stats.customCounters[name];
+    }
+    g_outputBuffer = "decMetric no such metric '" + name + "'\n";
+    errlog("Unable to decMetric: no such name '%s'", name);
+    return (uint64_t)0;
+  });
+  luaCtx.writeFunction("setMetric", [](const std::string& name, const double& value) {
+    if (g_stats.customGauges.count(name) > 0) {
+      g_stats.customGauges[name] = value;
+      return value;
+    }
+    g_outputBuffer = "setMetric no such metric '" + name + "'\n";
+    errlog("Unable to setMetric: no such name '%s'", name);
+    return 0.;
+  });
+  luaCtx.writeFunction("getMetric", [](const std::string& name) {
+    if (g_stats.customCounters.count(name) > 0) {
+      return (double)g_stats.customCounters[name].load();
+    } else if (g_stats.customGauges.count(name) > 0) {
+      return g_stats.customGauges[name].load();
+    }
+    g_outputBuffer = "getMetric no such metric '" + name + "'\n";
+    errlog("Unable to getMetric: no such name '%s'", name);
+    return 0.;
+  });
 }
 
 vector<std::function<void(void)>> setupLua(LuaContext& luaCtx, bool client, bool configCheck, const std::string& config)
index a60adaf216ca601d6c013aa61a73a01b65b05c20..9407c5523ba75400abb7343e9c9316e1c8da333b 100644 (file)
@@ -465,6 +465,8 @@ static void handlePrometheus(const YaHTTP::Request& req, YaHTTP::Response& resp)
 
     if (const auto& val = boost::get<pdns::stat_t*>(&std::get<1>(e)))
       output << (*val)->load();
+    else if (const auto& adval = boost::get<pdns::stat_t_trait<double>*>(&std::get<1>(e)))
+      output << (*adval)->load();
     else if (const auto& dval = boost::get<double*>(&std::get<1>(e)))
       output << **dval;
     else
@@ -851,12 +853,15 @@ static void handleJSONStats(const YaHTTP::Request& req, YaHTTP::Response& resp)
     for (const auto& e : g_stats.entries) {
       if (e.first == "special-memory-usage")
         continue; // Too expensive for get-all
-      if(const auto& val = boost::get<pdns::stat_t*>(&e.second))
+      if (const auto& val = boost::get<pdns::stat_t*>(&e.second)) {
         obj.insert({e.first, (double)(*val)->load()});
-      else if (const auto& dval = boost::get<double*>(&e.second))
+      } else if (const auto& adval = boost::get<pdns::stat_t_trait<double>*>(&e.second)) {
+        obj.insert({e.first, (*adval)->load()});
+      } else if (const auto& dval = boost::get<double*>(&e.second)) {
         obj.insert({e.first, (**dval)});
-      else
+      } else {
         obj.insert({e.first, (double)(*boost::get<DNSDistStats::statfunction_t>(&e.second))(e.first)});
+      }
     }
     Json my_json = obj;
     resp.body = my_json.dump();
@@ -1228,13 +1233,20 @@ static void handleStatsOnly(const YaHTTP::Request& req, YaHTTP::Response& resp)
     if (item.first == "special-memory-usage")
       continue; // Too expensive for get-all
 
-    if(const auto& val = boost::get<pdns::stat_t*>(&item.second)) {
+    if (const auto& val = boost::get<pdns::stat_t*>(&item.second)) {
       doc.push_back(Json::object {
           { "type", "StatisticItem" },
           { "name", item.first },
           { "value", (double)(*val)->load() }
         });
     }
+    else if(const auto& adval = boost::get<pdns::stat_t_trait<double>*>(&item.second)) {
+      doc.push_back(Json::object {
+          { "type", "StatisticItem" },
+          { "name", item.first },
+          { "value", (*adval)->load() }
+        });
+    }
     else if (const auto& dval = boost::get<double*>(&item.second)) {
       doc.push_back(Json::object {
           { "type", "StatisticItem" },
index feb383e5b53f110ec6bc47cbd602e7bbd5c83a1b..cad4862369d72fe84dfefadfde5e887914293444 100644 (file)
@@ -365,10 +365,10 @@ struct DNSDistStats
   stat_t tcpQueryPipeFull{0};
   stat_t tcpCrossProtocolQueryPipeFull{0};
   stat_t tcpCrossProtocolResponsePipeFull{0};
-
   double latencyAvg100{0}, latencyAvg1000{0}, latencyAvg10000{0}, latencyAvg1000000{0};
   typedef std::function<uint64_t(const std::string&)> statfunction_t;
-  typedef boost::variant<stat_t*, double*, statfunction_t> entry_t;
+  typedef boost::variant<stat_t*, pdns::stat_t_trait<double>*, double*, statfunction_t> entry_t;
+
   std::vector<std::pair<std::string, entry_t>> entries{
     {"responses", &responses},
     {"servfail-responses", &servfailResponses},
@@ -436,6 +436,8 @@ struct DNSDistStats
     {"latency-sum", &latencySum},
     {"latency-count", &latencyCount},
   };
+  std::map<std::string, stat_t> customCounters;
+  std::map<std::string, pdns::stat_t_trait<double> > customGauges;
 };
 
 extern struct DNSDistStats g_stats;
diff --git a/pdns/dnsdistdist/docs/reference/custommetrics.rst b/pdns/dnsdistdist/docs/reference/custommetrics.rst
new file mode 100644 (file)
index 0000000..22e0071
--- /dev/null
@@ -0,0 +1,58 @@
+Custom Metrics
+=====================================
+
+You can define at configuration time your own metrics that can be updated using lua.
+
+The first step is to declare a new metric using :func:`declareMetric`.
+
+Then you can update those at runtime using the following functions, depending on the metric type:
+
+ * manipulate counters using :func:`incMetric` and  :func:`decMetric`
+ * update a gauge using :func:`setMetric`
+
+.. function:: declareMetric(name, type) -> bool
+
+  .. versionadded:: 1.x
+
+  Return true if declaration was successful
+
+  :param str name: The name of the metric, lowercase alnum characters only
+  :param str type: The desired type in ``gauge`` or ``counter``
+
+.. function:: incMetric(name) -> int
+
+  .. versionadded:: 1.x
+
+  Increment counter by one, will issue an error if the metric is not declared or not a ``counter``
+  Return the new value
+
+  :param str name: The name of the metric
+
+.. function:: decMetric(name) -> int
+
+  .. versionadded:: 1.x
+
+  Decrement counter by one, will issue an error if the metric is not declared or not a ``counter``
+  Return the new value
+
+  :param str name: The name of the metric
+
+.. function:: getMetric(name) -> double
+
+  .. versionadded:: 1.x
+
+  Get metric value
+
+  :param str name: The name of the metric
+
+.. function:: setMetric(name, value) -> double
+
+  .. versionadded:: 1.x
+
+  Decrement counter by one, will issue an error if the metric is not declared or not a ``counter``
+  Return the new value
+
+  :param str name: The name of the metric
+
+
+                   
index 9bccb195c18a8163db25f2de590101f37e024ba6..e30237e83a5562fd5932db5e744fcc333ce3f2fc 100755 (executable)
@@ -24,4 +24,5 @@ These chapters contain extensive information on all functions and object availab
   kvs
   logging
   web
-  svc
\ No newline at end of file
+  svc
+  custommetrics
index d58dd7d80f684d61b97e1ccaa1d2c3b22be15a73..4b001f2edf44ced8cf4a5ceb2d07669dc8442dfc 100644 (file)
@@ -771,3 +771,36 @@ class TestWebConcurrentConnections(APITestsBase):
         r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout)
         self.assertTrue(r)
         self.assertEqual(r.status_code, 200)
+
+class TestAPICustomStatistics(APITestsBase):
+    __test__ = True
+    _maxConns = 2
+
+    _config_params = ['_testServerPort', '_webServerPort', '_webServerBasicAuthPasswordHashed', '_webServerAPIKeyHashed']
+    _config_template = """
+    newServer{address="127.0.0.1:%s"}
+    webserver("127.0.0.1:%s")
+    declareMetric("my-custom-metric", "counter")
+    declareMetric("my-other-metric", "counter")
+    declareMetric("my-gauge", "gauge")
+    setWebserverConfig({password="%s", apiKey="%s"})
+    """
+
+    def testCustomStats(self):
+        """
+        API: /jsonstat?command=stats
+        Test custom statistics are exposed
+        """
+        headers = {'x-api-key': self._webServerAPIKey}
+        url = 'http://127.0.0.1:' + str(self._webServerPort) + '/jsonstat?command=stats'
+        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()
+
+        expected = ['my-custom-metric', 'my-other-metric', 'my-gauge']
+
+        for key in expected:
+            self.assertIn(key, content)
+            self.assertTrue(content[key] >= 0)
index 2425c736c38bfc28dde760e087e43f4ef255bf04..a01119e5d43b730deea2ffc27c3d16f36f8557f6 100644 (file)
@@ -454,3 +454,66 @@ class TestProtocols(DNSDistTest):
         receivedQuery.id = query.id
         self.assertEqual(receivedQuery, query)
         self.assertEqual(receivedResponse, response)
+
+class TestCustomMetrics(DNSDistTest):
+    _config_template = """
+    function custommetrics(dq)
+      initialCounter = getMetric("my-custom-counter")
+      initialGauge = getMetric("my-custom-counter")
+      incMetric("my-custom-counter")
+      setMetric("my-custom-gauge", initialGauge + 1.3)
+      if getMetric("my-custom-counter") ~= (initialCounter + 1) or getMetric("my-custom-gauge") ~= (initialGauge + 1.3) then
+        return DNSAction.Spoof, '1.2.3.5'
+      end
+      return DNSAction.Spoof, '4.3.2.1'
+    end
+
+    function declareNewMetric(dq)
+      if declareMetric("new-runtime-metric", "counter") then
+        return DNSAction.Spoof, '1.2.3.4'
+      end
+      return DNSAction.None
+    end
+
+    declareMetric("my-custom-counter", "counter")
+    declareMetric("my-custom-gauge", "gauge")
+    addAction("declare.metric.advanced.tests.powerdns.com.", LuaAction(declareNewMetric))
+    addAction("operations.metric.advanced.tests.powerdns.com.", LuaAction(custommetrics))
+    newServer{address="127.0.0.1:%s"}
+    """
+
+    def testDeclareAfterConfig(self):
+        """
+        Advanced: Test custom metric declaration after config done
+        """
+        name = 'declare.metric.advanced.tests.powerdns.com.'
+        query = dns.message.make_query(name, 'A', 'IN')
+        response = dns.message.make_response(query)
+
+        for method in ("sendUDPQuery", "sendTCPQuery"):
+            sender = getattr(self, method)
+            (receivedQuery, receivedResponse) = sender(query, response)
+            receivedQuery.id = query.id
+            self.assertEqual(receivedQuery, query)
+            self.assertEqual(receivedResponse, response)
+
+    def testMetricOperations(self):
+        """
+        Advanced: Test basic operations on custom metrics
+        """
+        name = 'operations.metric.advanced.tests.powerdns.com.'
+        query = dns.message.make_query(name, 'A', 'IN')
+        # dnsdist set RA = RD for spoofed responses
+        query.flags &= ~dns.flags.RD
+        response = dns.message.make_response(query)
+        rrset = dns.rrset.from_text(name,
+                                    60,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.A,
+                                    '4.3.2.1')
+        response.answer.append(rrset)
+
+        for method in ("sendUDPQuery", "sendTCPQuery"):
+            sender = getattr(self, method)
+            (_, receivedResponse) = sender(query, response=None, useQueue=False)
+            self.assertEqual(receivedResponse, response)