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
#include <sys/socket.h>
#include <net/if.h>
+#include <regex>
#include <sys/types.h>
#include <sys/stat.h>
#include <thread>
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)
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
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();
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" },
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},
{"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;
--- /dev/null
+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
+
+
+
kvs
logging
web
- svc
\ No newline at end of file
+ svc
+ custommetrics
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)
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)