From c4045cc7544e3135985206c2fdaa9943c6edef5c Mon Sep 17 00:00:00 2001 From: marinierb Date: Fri, 13 Mar 2026 16:57:35 -0400 Subject: [PATCH 1/2] prometheus-node-exporter-lua: add unbound stats collector Signed-off-by: Bruno Marinier --- utils/prometheus-node-exporter-lua/Makefile | 14 +++++- .../lib/lua/prometheus-collectors/unbound.lua | 46 +++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 utils/prometheus-node-exporter-lua/files/usr/lib/lua/prometheus-collectors/unbound.lua diff --git a/utils/prometheus-node-exporter-lua/Makefile b/utils/prometheus-node-exporter-lua/Makefile index 31583a6cf8df13..7c0aa6c537fa6b 100644 --- a/utils/prometheus-node-exporter-lua/Makefile +++ b/utils/prometheus-node-exporter-lua/Makefile @@ -4,7 +4,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=prometheus-node-exporter-lua -PKG_VERSION:=2025.11.22 +PKG_VERSION:=2026.03.13 PKG_RELEASE:=1 PKG_MAINTAINER:=Etienne CHAMPETIER @@ -279,6 +279,17 @@ define Package/prometheus-node-exporter-lua-nft-counters/install $(INSTALL_DATA) ./files/usr/lib/lua/prometheus-collectors/nft_counters.lua $(1)/usr/lib/lua/prometheus-collectors/ endef +define Package/prometheus-node-exporter-lua-unbound + $(call Package/prometheus-node-exporter-lua/Default) + TITLE+= (unbound stats collector) + DEPENDS:=prometheus-node-exporter-lua +unbound-control +endef + +define Package/prometheus-node-exporter-lua-unbound/install + $(INSTALL_DIR) $(1)/usr/lib/lua/prometheus-collectors + $(INSTALL_DATA) ./files/usr/lib/lua/prometheus-collectors/unbound.lua $(1)/usr/lib/lua/prometheus-collectors/ +endef + $(eval $(call BuildPackage,prometheus-node-exporter-lua)) $(eval $(call BuildPackage,prometheus-node-exporter-lua-bmx7)) $(eval $(call BuildPackage,prometheus-node-exporter-lua-dawn)) @@ -300,3 +311,4 @@ $(eval $(call BuildPackage,prometheus-node-exporter-lua-realtek-poe)) $(eval $(call BuildPackage,prometheus-node-exporter-lua-mwan3)) $(eval $(call BuildPackage,prometheus-node-exporter-lua-ethtool)) $(eval $(call BuildPackage,prometheus-node-exporter-lua-nft-counters)) +$(eval $(call BuildPackage,prometheus-node-exporter-lua-unbound)) diff --git a/utils/prometheus-node-exporter-lua/files/usr/lib/lua/prometheus-collectors/unbound.lua b/utils/prometheus-node-exporter-lua/files/usr/lib/lua/prometheus-collectors/unbound.lua new file mode 100644 index 00000000000000..12117d242cfc2b --- /dev/null +++ b/utils/prometheus-node-exporter-lua/files/usr/lib/lua/prometheus-collectors/unbound.lua @@ -0,0 +1,46 @@ +local function scrape() + local metrics = { + ["total.num.queries" ] = metric("unbound_num_queries_total", "counter"), + ["total.num.queries.ip.ratelimited" ] = metric("unbound_num_queries_ip_ratelimited_total", "counter"), + ["total.num.queries.cookie.valid" ] = metric("unbound_num_queries_cookie_valid_total", "counter"), + ["total.num.queries.cookie.client" ] = metric("unbound_num_queries_cookie_client_total", "counter"), + ["total.num.queries.cookie.invalid" ] = metric("unbound_num_queries_cookie_invalid_total", "counter"), + ["total.num.queries.discard.timeout"] = metric("unbound_num_queries_discard_timeout_total","counter"), + ["total.num.queries.wait.limit" ] = metric("unbound_num_queries_wait_limit_total", "counter"), + ["total.num.cachehits" ] = metric("unbound_cachehits_total", "counter"), + ["total.num.cachemiss" ] = metric("unbound_cachemiss_total", "counter"), + ["total.num.prefetch" ] = metric("unbound_prefetch_total", "counter"), + ["total.num.queries.timed.out" ] = metric("unbound_num_queries_timed_out_total", "counter"), + ["total.query.queue.time.us.max" ] = metric("unbound_query_queue_time_us_max_total", "counter"), + ["total.num.expired" ] = metric("unbound_num_expired_total", "counter"), + ["total.num.recursivereplies" ] = metric("unbound_num_recursivereplies_total", "counter"), + ["total.num.dns.error.reports" ] = metric("unbound_num_dns_error_reports_total", "counter"), + ["total.requestlist.avg" ] = metric("unbound_requestlist_avg_total", "counter"), + ["total.requestlist.max" ] = metric("unbound_requestlist_max_total", "counter"), + ["total.requestlist.overwritten" ] = metric("unbound_requestlist_overwritten_total", "counter"), + ["total.requestlist.exceeded" ] = metric("unbound_requestlist_exceeded_total", "counter"), + ["total.requestlist.current.all" ] = metric("unbound_requestlist_current_all_total", "counter"), + ["total.requestlist.current.user" ] = metric("unbound_requestlist_current_user_total", "counter"), + ["total.recursion.time.avg" ] = metric("unbound_recursion_time_avg_total", "counter"), + ["total.recursion.time.median" ] = metric("unbound_recursion_time_median_total", "counter"), + } + + local handle = io.popen("/usr/sbin/unbound-control stats_noreset | sed 's/_/./g'") + if not handle then + return nil, "failed to run unbound-control" + end + local out = handle:read("*a") + handle:close() + + for line in out:gmatch("[^\r\n]+") do + local key, val = line:match("^([%w%.]+)=(%-?[%d%.]+)$") + if key and val then + local n = tonumber(val) + if metrics[key] then + metrics[key]({}, n) + end + end + end +end + +return { scrape = scrape } From ab0c5a8175ec507aee904d4b63c018961d9e19bd Mon Sep 17 00:00:00 2001 From: marinierb Date: Sat, 28 Mar 2026 16:46:28 -0400 Subject: [PATCH 2/2] prometheus-node-exporter-lua: add unbound stats collector Signed-off-by: marinierb Signed-off-by: marinierb --- utils/prometheus-node-exporter-lua/Makefile | 2 +- .../lib/lua/prometheus-collectors/unbound.lua | 62 ++++++++++++------- 2 files changed, 42 insertions(+), 22 deletions(-) diff --git a/utils/prometheus-node-exporter-lua/Makefile b/utils/prometheus-node-exporter-lua/Makefile index 7c0aa6c537fa6b..081ce8cc195ca1 100644 --- a/utils/prometheus-node-exporter-lua/Makefile +++ b/utils/prometheus-node-exporter-lua/Makefile @@ -4,7 +4,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=prometheus-node-exporter-lua -PKG_VERSION:=2026.03.13 +PKG_VERSION:=2026.03.28 PKG_RELEASE:=1 PKG_MAINTAINER:=Etienne CHAMPETIER diff --git a/utils/prometheus-node-exporter-lua/files/usr/lib/lua/prometheus-collectors/unbound.lua b/utils/prometheus-node-exporter-lua/files/usr/lib/lua/prometheus-collectors/unbound.lua index 12117d242cfc2b..2cf3b1c5adcf1b 100644 --- a/utils/prometheus-node-exporter-lua/files/usr/lib/lua/prometheus-collectors/unbound.lua +++ b/utils/prometheus-node-exporter-lua/files/usr/lib/lua/prometheus-collectors/unbound.lua @@ -1,39 +1,59 @@ +-- Unbound stats exporter + +local socket = require("socket") +local unix = require("socket.unix") + local function scrape() local metrics = { ["total.num.queries" ] = metric("unbound_num_queries_total", "counter"), - ["total.num.queries.ip.ratelimited" ] = metric("unbound_num_queries_ip_ratelimited_total", "counter"), - ["total.num.queries.cookie.valid" ] = metric("unbound_num_queries_cookie_valid_total", "counter"), - ["total.num.queries.cookie.client" ] = metric("unbound_num_queries_cookie_client_total", "counter"), - ["total.num.queries.cookie.invalid" ] = metric("unbound_num_queries_cookie_invalid_total", "counter"), - ["total.num.queries.discard.timeout"] = metric("unbound_num_queries_discard_timeout_total","counter"), - ["total.num.queries.wait.limit" ] = metric("unbound_num_queries_wait_limit_total", "counter"), + ["total.num.queries_ip_ratelimited" ] = metric("unbound_num_queries_ip_ratelimited_total", "counter"), + ["total.num.queries_cookie_valid" ] = metric("unbound_num_queries_cookie_valid_total", "counter"), + ["total.num.queries_cookie_client" ] = metric("unbound_num_queries_cookie_client_total", "counter"), + ["total.num.queries_cookie_invalid" ] = metric("unbound_num_queries_cookie_invalid_total", "counter"), + ["total.num.queries_discard_timeout"] = metric("unbound_num_queries_discard_timeout_total","counter"), + ["total.num.queries_wait_limit" ] = metric("unbound_num_queries_wait_limit_total", "counter"), ["total.num.cachehits" ] = metric("unbound_cachehits_total", "counter"), ["total.num.cachemiss" ] = metric("unbound_cachemiss_total", "counter"), ["total.num.prefetch" ] = metric("unbound_prefetch_total", "counter"), - ["total.num.queries.timed.out" ] = metric("unbound_num_queries_timed_out_total", "counter"), - ["total.query.queue.time.us.max" ] = metric("unbound_query_queue_time_us_max_total", "counter"), + ["total.num.queries_timed_out" ] = metric("unbound_num_queries_timed_out_total", "counter"), + ["total.query.queue_time_us.max" ] = metric("unbound_query_queue_time_us_max", "gauge"), ["total.num.expired" ] = metric("unbound_num_expired_total", "counter"), ["total.num.recursivereplies" ] = metric("unbound_num_recursivereplies_total", "counter"), - ["total.num.dns.error.reports" ] = metric("unbound_num_dns_error_reports_total", "counter"), - ["total.requestlist.avg" ] = metric("unbound_requestlist_avg_total", "counter"), - ["total.requestlist.max" ] = metric("unbound_requestlist_max_total", "counter"), + ["total.num.dns_error_reports" ] = metric("unbound_num_dns_error_reports_total", "counter"), + ["total.requestlist.avg" ] = metric("unbound_requestlist_avg", "gauge"), + ["total.requestlist.max" ] = metric("unbound_requestlist_max", "gauge"), ["total.requestlist.overwritten" ] = metric("unbound_requestlist_overwritten_total", "counter"), ["total.requestlist.exceeded" ] = metric("unbound_requestlist_exceeded_total", "counter"), - ["total.requestlist.current.all" ] = metric("unbound_requestlist_current_all_total", "counter"), - ["total.requestlist.current.user" ] = metric("unbound_requestlist_current_user_total", "counter"), - ["total.recursion.time.avg" ] = metric("unbound_recursion_time_avg_total", "counter"), - ["total.recursion.time.median" ] = metric("unbound_recursion_time_median_total", "counter"), + ["total.requestlist.current.all" ] = metric("unbound_requestlist_current_all", "gauge"), + ["total.requestlist.current.user" ] = metric("unbound_requestlist_current_user", "gauge"), + ["total.recursion.time.avg" ] = metric("unbound_recursion_time_avg", "gauge"), + ["total.recursion.time.median" ] = metric("unbound_recursion_time_median", "gauge"), } - local handle = io.popen("/usr/sbin/unbound-control stats_noreset | sed 's/_/./g'") - if not handle then - return nil, "failed to run unbound-control" + local sock = unix() + local ok, err = sock:connect("/run/unbound.ctl") + if not ok then + return nil, "failed to connect to unbound socket: " .. (err or "unknown") end - local out = handle:read("*a") - handle:close() + + sock:settimeout(1) + sock:send("UBCT1 stats_noreset\n") + + local chunks = {} + while true do + local chunk, err, partial = sock:receive(4096) + if partial and partial ~= "" then + chunks[#chunks + 1] = partial + end + if not chunk then break end + chunks[#chunks + 1] = chunk + end + sock:close() + + local out = table.concat(chunks) for line in out:gmatch("[^\r\n]+") do - local key, val = line:match("^([%w%.]+)=(%-?[%d%.]+)$") + local key, val = line:match("^([%w%._]+)=(%-?[%d%.]+)$") if key and val then local n = tonumber(val) if metrics[key] then