prometheus-node-exporter-ucode: add new package 20007/head
authorAndre Heider <a.heider@gmail.com>
Fri, 2 Dec 2022 13:45:01 +0000 (14:45 +0100)
committerAndre Heider <a.heider@gmail.com>
Fri, 25 Aug 2023 08:18:26 +0000 (10:18 +0200)
This is a port of prometheus-node-exporter-lua to ucode.

Signed-off-by: Andre Heider <a.heider@gmail.com>
25 files changed:
utils/prometheus-node-exporter-ucode/Makefile [new file with mode: 0644]
utils/prometheus-node-exporter-ucode/files/base/conntrack.uc [new file with mode: 0644]
utils/prometheus-node-exporter-ucode/files/base/cpu.uc [new file with mode: 0644]
utils/prometheus-node-exporter-ucode/files/base/entropy.uc [new file with mode: 0644]
utils/prometheus-node-exporter-ucode/files/base/filefd.uc [new file with mode: 0644]
utils/prometheus-node-exporter-ucode/files/base/loadavg.uc [new file with mode: 0644]
utils/prometheus-node-exporter-ucode/files/base/meminfo.uc [new file with mode: 0644]
utils/prometheus-node-exporter-ucode/files/base/netclass.uc [new file with mode: 0644]
utils/prometheus-node-exporter-ucode/files/base/netdev.uc [new file with mode: 0644]
utils/prometheus-node-exporter-ucode/files/base/selinux.uc [new file with mode: 0644]
utils/prometheus-node-exporter-ucode/files/base/time.uc [new file with mode: 0644]
utils/prometheus-node-exporter-ucode/files/base/uname.uc [new file with mode: 0644]
utils/prometheus-node-exporter-ucode/files/config [new file with mode: 0644]
utils/prometheus-node-exporter-ucode/files/extra/dnsmasq.uc [new file with mode: 0644]
utils/prometheus-node-exporter-ucode/files/extra/ltq-dsl.uc [new file with mode: 0644]
utils/prometheus-node-exporter-ucode/files/extra/netstat.uc [new file with mode: 0644]
utils/prometheus-node-exporter-ucode/files/extra/openwrt.uc [new file with mode: 0644]
utils/prometheus-node-exporter-ucode/files/extra/snmp6.uc [new file with mode: 0644]
utils/prometheus-node-exporter-ucode/files/extra/uci_dhcp_host.uc [new file with mode: 0644]
utils/prometheus-node-exporter-ucode/files/extra/wifi.uc [new file with mode: 0644]
utils/prometheus-node-exporter-ucode/files/extra/wireguard.uc [new file with mode: 0644]
utils/prometheus-node-exporter-ucode/files/init [new file with mode: 0644]
utils/prometheus-node-exporter-ucode/files/metrics.uc [new file with mode: 0644]
utils/prometheus-node-exporter-ucode/files/run.sh [new file with mode: 0755]
utils/prometheus-node-exporter-ucode/test.sh [new file with mode: 0755]

diff --git a/utils/prometheus-node-exporter-ucode/Makefile b/utils/prometheus-node-exporter-ucode/Makefile
new file mode 100644 (file)
index 0000000..3d712c1
--- /dev/null
@@ -0,0 +1,74 @@
+# Copyright (C) 2013-2017 OpenWrt.org
+
+include $(TOPDIR)/rules.mk
+
+PKG_NAME:=prometheus-node-exporter-ucode
+PKG_VERSION:=2022.12.02
+PKG_RELEASE:=1
+
+PKG_MAINTAINER:=Andre Heider <a.heider@gmail.com>
+PKG_LICENSE:=Apache-2.0
+
+include $(INCLUDE_DIR)/package.mk
+
+Build/Compile=
+
+define Package/$(PKG_NAME)/Default
+  SECTION:=utils
+  CATEGORY:=Utilities
+  TITLE:=Prometheus node exporter
+  PKGARCH:=all
+endef
+
+define Package/$(PKG_NAME)
+  $(call Package/$(PKG_NAME)/Default)
+  DEPENDS:=+uhttpd +uhttpd-mod-ucode +rpcd +ucode-mod-fs +ucode-mod-ubus
+endef
+
+define Package/$(PKG_NAME)/install
+       $(INSTALL_DIR) $(1)/etc/config
+       $(INSTALL_CONF) ./files/config $(1)/etc/config/$(PKG_NAME)
+       $(INSTALL_DIR) $(1)/etc/init.d
+       $(INSTALL_BIN) ./files/init $(1)/etc/init.d/$(PKG_NAME)
+       $(INSTALL_DIR) $(1)/usr/share/ucode/node-exporter/lib
+       $(INSTALL_DATA) ./files/metrics.uc $(1)/usr/share/ucode/node-exporter/
+       $(INSTALL_DATA) ./files/base/*.uc $(1)/usr/share/ucode/node-exporter/lib/
+       $(INSTALL_DIR) $(1)/usr/bin
+       $(INSTALL_BIN) ./files/run.sh $(1)/usr/bin/$(PKG_NAME)
+endef
+
+define Package/$(PKG_NAME)/conffiles
+/etc/config/$(PKG_NAME)
+endef
+
+define Package/$(PKG_NAME)/description
+  Provides node metrics as Prometheus scraping endpoint.
+
+  This service is a lightweight rewrite in ucode of the offical Prometheus node_exporter.
+endef
+
+$(eval $(call BuildPackage,prometheus-node-exporter-ucode))
+
+define Collector
+  define Package/$(PKG_NAME)-$(1)
+    $$(call Package/$(PKG_NAME)/Default)
+    TITLE+= ($(2))
+    DEPENDS:=$(PKG_NAME) $(3)
+  endef
+
+  define Package/$(PKG_NAME)-$(1)/install
+       $$(INSTALL_DIR) $$(1)/usr/share/ucode/node-exporter/lib
+       $$(INSTALL_DATA) ./files/extra/$(1).uc $$(1)/usr/share/ucode/node-exporter/lib/
+  endef
+
+  $$(eval $$(call BuildPackage,$(PKG_NAME)-$(1)))
+endef
+
+$(eval $(call Collector,dnsmasq,Dnsmasq collector,@dnsmasq))
+$(eval $(call Collector,ltq-dsl,Lantiq/Intel/MaxLinear DSL collector,@ltq-dsl-app))
+$(eval $(call Collector,netstat,netstat collector,))
+$(eval $(call Collector,openwrt,OpenWrt collector,))
+$(eval $(call Collector,snmp6,snmp6 collector,))
+$(eval $(call Collector,uci_dhcp_host,UCI DHCP host collector,))
+$(eval $(call Collector,wifi,Wi-Fi collector,+ucode-mod-nl80211))
+$(eval $(call Collector,wireguard,Wireguard collector,+rpcd-mod-wireguard))
diff --git a/utils/prometheus-node-exporter-ucode/files/base/conntrack.uc b/utils/prometheus-node-exporter-ucode/files/base/conntrack.uc
new file mode 100644 (file)
index 0000000..b3ee3f3
--- /dev/null
@@ -0,0 +1,4 @@
+gauge("node_nf_conntrack_entries")
+       (null, oneline("/proc/sys/net/netfilter/nf_conntrack_count"));
+gauge("node_nf_conntrack_entries_limit")
+       (null, oneline("/proc/sys/net/netfilter/nf_conntrack_max"));
diff --git a/utils/prometheus-node-exporter-ucode/files/base/cpu.uc b/utils/prometheus-node-exporter-ucode/files/base/cpu.uc
new file mode 100644 (file)
index 0000000..574655d
--- /dev/null
@@ -0,0 +1,44 @@
+let f = fs.open("/proc/stat");
+
+if (!f)
+       return false;
+
+const desc = [
+       null,
+       "user",
+       "nice",
+       "system",
+       "idle",
+       "iowait",
+       "irq",
+       "softirq",
+       "steal",
+       "guest",
+       "guest_nice",
+];
+const m_cpu = counter("node_cpu_seconds_total");
+
+let line;
+while (line = nextline(f)) {
+       const x = wsplit(line);
+
+       if (length(x) < 2)
+               continue;
+
+       if (match(x[0], /^cpu\d+/)) {
+               const count = min(length(x), length(desc));
+               for (let i = 1; i < count; i++)
+                       m_cpu({ cpu: x[0], mode: desc[i] }, x[i] / 100.0);
+       } else if (x[0] == "intr")
+               counter("node_intr_total")(null, x[1]);
+       else if (x[0] == "ctxt")
+               counter("node_context_switches_total")(null, x[1]);
+       else if (x[0] == "btime")
+               gauge("node_boot_time_seconds")(null, x[1]);
+       else if (x[0] == "processes")
+               counter("node_forks_total")(null, x[1]);
+       else if (x[0] == "procs_running")
+               gauge("node_procs_running_total")(null, x[1]);
+       else if (x[0] == "procs_blocked")
+               gauge("node_procs_blocked_total")(null, x[1]);
+}
diff --git a/utils/prometheus-node-exporter-ucode/files/base/entropy.uc b/utils/prometheus-node-exporter-ucode/files/base/entropy.uc
new file mode 100644 (file)
index 0000000..2df4426
--- /dev/null
@@ -0,0 +1,4 @@
+gauge("node_entropy_available_bits")
+       (null, oneline("/proc/sys/kernel/random/entropy_avail"));
+gauge("node_entropy_pool_size_bits")
+       (null, oneline("/proc/sys/kernel/random/poolsize"));
diff --git a/utils/prometheus-node-exporter-ucode/files/base/filefd.uc b/utils/prometheus-node-exporter-ucode/files/base/filefd.uc
new file mode 100644 (file)
index 0000000..359cffd
--- /dev/null
@@ -0,0 +1,7 @@
+const x = wsplit(oneline("/proc/sys/fs/file-nr"));
+
+if (length(x) < 3)
+       return false;
+
+gauge("node_filefd_allocated")(null, x[0]);
+gauge("node_filefd_maximum")(null, x[2]);
diff --git a/utils/prometheus-node-exporter-ucode/files/base/loadavg.uc b/utils/prometheus-node-exporter-ucode/files/base/loadavg.uc
new file mode 100644 (file)
index 0000000..ba67daf
--- /dev/null
@@ -0,0 +1,8 @@
+const x = wsplit(oneline("/proc/loadavg"));
+
+if (length(x) < 3)
+       return false;
+
+gauge("node_load1")(null, x[0]);
+gauge("node_load5")(null, x[1]);
+gauge("node_load15")(null, x[2]);
diff --git a/utils/prometheus-node-exporter-ucode/files/base/meminfo.uc b/utils/prometheus-node-exporter-ucode/files/base/meminfo.uc
new file mode 100644 (file)
index 0000000..3cecb12
--- /dev/null
@@ -0,0 +1,24 @@
+let f = fs.open("/proc/meminfo");
+
+if (!f)
+       return false;
+
+let line;
+while (line = nextline(f)) {
+       const x = wsplit(line);
+
+       if (length(x) < 2)
+               continue;
+
+       if (substr(x[0], -1) != ":")
+               continue;
+
+       let name;
+       if (substr(x[0], -2) == "):")
+               name = replace(substr(x[0], 0, -2), "(", "_");
+       else
+               name = substr(x[0], 0, -1);
+
+       gauge(`node_memory_${name}_bytes`)
+               (null, x[2] == "kB" ? x[1] * 1024 : x[1]);
+}
diff --git a/utils/prometheus-node-exporter-ucode/files/base/netclass.uc b/utils/prometheus-node-exporter-ucode/files/base/netclass.uc
new file mode 100644 (file)
index 0000000..10b3cfd
--- /dev/null
@@ -0,0 +1,48 @@
+const root = "/sys/class/net/";
+const devices = fs.lsdir(root);
+
+if (length(devices) < 1)
+       return false;
+
+const m_info = gauge("node_network_info");
+const m_speed = gauge("node_network_speed_bytes");
+const metrics = {
+       addr_assign_type:       gauge("node_network_address_assign_type"),
+       carrier:                gauge("node_network_carrier"),
+       carrier_changes:        counter("node_network_carrier_changes_total"),
+       carrier_down_count:     counter("node_network_carrier_down_changes_total"),
+       carrier_up_count:       counter("node_network_carrier_up_changes_total"),
+       dev_id:                 gauge("node_network_device_id"),
+       dormant:                gauge("node_network_dormant"),
+       flags:                  gauge("node_network_flags"),
+       ifindex:                gauge("node_network_iface_id"),
+       iflink:                 gauge("node_network_iface_link"),
+       link_mode:              gauge("node_network_iface_link_mode"),
+       mtu:                    gauge("node_network_mtu_bytes"),
+       name_assign_type:       gauge("node_network_name_assign_type"),
+       netdev_group:           gauge("node_network_net_dev_group"),
+       type:                   gauge("node_network_protocol_type"),
+       tx_queue_len:           gauge("node_network_transmit_queue_length"),
+};
+
+for (let device in devices) {
+       const devroot = root + device + "/";
+
+       m_info({
+               device,
+               address:        oneline(devroot + "address"),
+               broadcast:      oneline(devroot + "broadcast"),
+               duplex:         oneline(devroot + "duplex"),
+               operstate:      oneline(devroot + "operstate"),
+               ifalias:        oneline(devroot + "ifalias"),
+       }, 1);
+
+       for (let m in metrics) {
+               let line = oneline(devroot + m);
+               metrics[m]({ device }, line);
+       }
+
+       const speed = int(oneline(devroot + "speed"));
+       if (speed > 0)
+                       m_speed({ device }, speed * 1000 * 1000 / 8);
+}
diff --git a/utils/prometheus-node-exporter-ucode/files/base/netdev.uc b/utils/prometheus-node-exporter-ucode/files/base/netdev.uc
new file mode 100644 (file)
index 0000000..f8fc68d
--- /dev/null
@@ -0,0 +1,40 @@
+let f = fs.open("/proc/net/dev");
+
+if (!f)
+       return false;
+
+const m = [
+       null,
+       counter("node_network_receive_bytes_total"),
+       counter("node_network_receive_packets_total"),
+       counter("node_network_receive_errs_total"),
+       counter("node_network_receive_drop_total"),
+       counter("node_network_receive_fifo_total"),
+       counter("node_network_receive_frame_total"),
+       counter("node_network_receive_compressed_total"),
+       counter("node_network_receive_multicast_total"),
+       counter("node_network_transmit_bytes_total"),
+       counter("node_network_transmit_packets_total"),
+       counter("node_network_transmit_errs_total"),
+       counter("node_network_transmit_drop_total"),
+       counter("node_network_transmit_fifo_total"),
+       counter("node_network_transmit_colls_total"),
+       counter("node_network_transmit_carrier_total"),
+       counter("node_network_transmit_compressed_total"),
+];
+
+let line;
+while (line = nextline(f)) {
+       const x = wsplit(ltrim(line), " ");
+
+       if (length(x) < 2)
+               continue;
+
+       if (substr(x[0], -1) != ":")
+               continue;
+
+       const count = min(length(x), length(m));
+       const labels = { device: substr(x[0], 0, -1) };
+       for (let i = 1; i < count; i++)
+               m[i](labels, x[i]);
+}
diff --git a/utils/prometheus-node-exporter-ucode/files/base/selinux.uc b/utils/prometheus-node-exporter-ucode/files/base/selinux.uc
new file mode 100644 (file)
index 0000000..11840a8
--- /dev/null
@@ -0,0 +1,10 @@
+const mode = oneline("/sys/fs/selinux/enforce");
+const enabled = gauge("node_selinux_enabled");
+
+if (mode == null) {
+       enabled(null, 0);
+       return;
+}
+
+enabled(null, 1);
+gauge("node_selinux_current_mode")(null, mode);
diff --git a/utils/prometheus-node-exporter-ucode/files/base/time.uc b/utils/prometheus-node-exporter-ucode/files/base/time.uc
new file mode 100644 (file)
index 0000000..7d13ea8
--- /dev/null
@@ -0,0 +1 @@
+gauge("node_time_seconds")(null, time());
diff --git a/utils/prometheus-node-exporter-ucode/files/base/uname.uc b/utils/prometheus-node-exporter-ucode/files/base/uname.uc
new file mode 100644 (file)
index 0000000..50cb352
--- /dev/null
@@ -0,0 +1,8 @@
+gauge("node_uname_info")({
+       sysname:        oneline("/proc/sys/kernel/ostype"),
+       nodename:       oneline("/proc/sys/kernel/hostname"),
+       release:        oneline("/proc/sys/kernel/osrelease"),
+       version:        oneline("/proc/sys/kernel/version"),
+       machine:        poneline("uname -m"), // TODO lame
+       domainname:     oneline("/proc/sys/kernel/domainname"),
+}, 1);
diff --git a/utils/prometheus-node-exporter-ucode/files/config b/utils/prometheus-node-exporter-ucode/files/config
new file mode 100644 (file)
index 0000000..8741f4a
--- /dev/null
@@ -0,0 +1,7 @@
+config prometheus-node-exporter-ucode 'main'
+       option listen_interface 'loopback'
+       option listen_port '9101'
+       option http_keepalive '70'
+
+config collector 'wifi'
+       option stations '1'
diff --git a/utils/prometheus-node-exporter-ucode/files/extra/dnsmasq.uc b/utils/prometheus-node-exporter-ucode/files/extra/dnsmasq.uc
new file mode 100644 (file)
index 0000000..3644b20
--- /dev/null
@@ -0,0 +1,6 @@
+const x = ubus.call("dnsmasq", "metrics");
+if (!x)
+       return false;
+
+for (let i in x)
+       gauge(`dnsmasq_${i}_total`)(null, x[i]);
diff --git a/utils/prometheus-node-exporter-ucode/files/extra/ltq-dsl.uc b/utils/prometheus-node-exporter-ucode/files/extra/ltq-dsl.uc
new file mode 100644 (file)
index 0000000..1644497
--- /dev/null
@@ -0,0 +1,72 @@
+const x = ubus.call("dsl", "metrics");
+
+if (!x)
+       return false;
+
+gauge("dsl_info")({
+       atuc_vendor:            x.atu_c.vendor,
+       atuc_system_vendor:     x.atu_c.system_vendor,
+       chipset:                x.chipset,
+       firmware_version:       x.firmware_version,
+       api_version:            x.api_version,
+       driver_version:         x.driver_version,
+}, 1);
+
+gauge("dsl_line_info")({
+       annex:          x.annex,
+       standard:       x.standard,
+       mode:           x.mode,
+       profile:        x.profile,
+}, 1);
+
+gauge("dsl_up")({ detail: x.state }, x.up);
+gauge("dsl_uptime_seconds")(null, x.uptime);
+
+gauge("dsl_line_attenuation_db")
+       ({ direction: "down" }, x.downstream.latn)
+       ({ direction: "up" }, x.upstream.latn);
+gauge("dsl_signal_attenuation_db")
+       ({ direction: "down" }, x.downstream.satn)
+       ({ direction: "up" }, x.upstream.satn);
+gauge("dsl_signal_to_noise_margin_db")
+       ({ direction: "down" }, x.downstream.snr)
+       ({ direction: "up" }, x.upstream.snr);
+gauge("dsl_aggregated_transmit_power_db")
+       ({ direction: "down" }, x.downstream.actatp)
+       ({ direction: "up" }, x.upstream.actatp);
+
+if (x.downstream.interleave_delay)
+       gauge("dsl_latency_seconds")
+               ({ direction: "down" }, x.downstream.interleave_delay / 1000000.0)
+               ({ direction: "up" }, x.upstream.interleave_delay / 1000000.0);
+gauge("dsl_datarate")
+       ({ direction: "down" }, x.downstream.data_rate)
+       ({ direction: "up" }, x.upstream.data_rate);
+gauge("dsl_max_datarate")
+       ({ direction: "down" }, x.downstream.attndr)
+       ({ direction: "up" }, x.upstream.attndr);
+
+counter("dsl_error_seconds_total")
+       ({ err: "forward error correction", loc: "near" }, x.errors.near.fecs)
+       ({ err: "forward error correction", loc: "far" }, x.errors.far.fecs)
+       ({ err: "errored", loc: "near" }, x.errors.near.es)
+       ({ err: "errored", loc: "far" }, x.errors.far.es)
+       ({ err: "severely errored", loc: "near" }, x.errors.near.ses)
+       ({ err: "severely errored", loc: "far" }, x.errors.far.ses)
+       ({ err: "loss of signal", loc: "near" }, x.errors.near.loss)
+       ({ err: "loss of signal", loc: "far" }, x.errors.far.loss)
+       ({ err: "unavailable", loc: "near" }, x.errors.near.uas)
+       ({ err: "unavailable", loc: "far" }, x.errors.far.uas);
+
+counter("dsl_errors_total")
+       ({ err: "header error code error", loc: "near" }, x.errors.near.hec)
+       ({ err: "header error code error", loc: "far" }, x.errors.far.hec)
+       ({ err: "non pre-emptive crc error", loc: "near" }, x.errors.near.crc_p)
+       ({ err: "non pre-emptive crc error", loc: "far" }, x.errors.far.crc_p)
+       ({ err: "pre-emptive crc error", loc: "near" }, x.errors.near.crcp_p)
+       ({ err: "pre-emptive crc error", loc: "far" }, x.errors.far.crcp_p);
+
+if (x.erb)
+       counter("dsl_erb_total")
+               ({ counter: "sent" }, x.erb.sent)
+               ({ counter: "discarded" }, x.erb.discarded);
diff --git a/utils/prometheus-node-exporter-ucode/files/extra/netstat.uc b/utils/prometheus-node-exporter-ucode/files/extra/netstat.uc
new file mode 100644 (file)
index 0000000..7449305
--- /dev/null
@@ -0,0 +1,30 @@
+function parse(fn) {
+       let f = fs.open(fn);
+
+       if (!f)
+               return false;
+
+       let names, values;
+       while (names = nextline(f), values = nextline(f)) {
+               const name = wsplit(names);
+               const value = wsplit(values);
+
+               if (name[0] != value[0])
+                       continue;
+
+               if (length(name) != length(value))
+                       continue;
+
+               let prefix = substr(name[0], 0, -1);
+               for (let i = 1; i < length(name); i++)
+                       gauge(`node_netstat_${prefix}_${name[i]}`)(null, value[i]);
+       }
+
+       return true;
+}
+
+let n = parse("/proc/net/netstat");
+let s = parse("/proc/net/snmp");
+
+if (!n && !s)
+       return false;
diff --git a/utils/prometheus-node-exporter-ucode/files/extra/openwrt.uc b/utils/prometheus-node-exporter-ucode/files/extra/openwrt.uc
new file mode 100644 (file)
index 0000000..10c15a1
--- /dev/null
@@ -0,0 +1,14 @@
+const x = ubus.call("system", "board");
+
+if (!x)
+       return false;
+
+gauge("node_openwrt_info")({
+       board_name:     x.board_name,
+       id:             x.release.distribution,
+       model:          x.model,
+       release:        x.release.version,
+       revision:       x.release.revision,
+       system:         x.system,
+       target:         x.release.target,
+}, 1);
diff --git a/utils/prometheus-node-exporter-ucode/files/extra/snmp6.uc b/utils/prometheus-node-exporter-ucode/files/extra/snmp6.uc
new file mode 100644 (file)
index 0000000..d440a88
--- /dev/null
@@ -0,0 +1,23 @@
+function parse(fn, device, skipdecl) {
+       let f = fs.open(fn);
+
+       if (!f)
+               return false;
+
+       const labels = { device };
+       let line;
+       while (line = nextline(f)) {
+               const x = wsplit(line);
+
+               if (length(x) < 2)
+                       continue;
+
+               counter(`snmp6_${x[0]}`, null, skipdecl)(labels, x[1]);
+       }
+}
+
+parse("/proc/net/snmp6", "all");
+
+const root = "/proc/net/dev_snmp6/";
+for (let device in fs.lsdir(root))
+       parse(root + device, device, true);
diff --git a/utils/prometheus-node-exporter-ucode/files/extra/uci_dhcp_host.uc b/utils/prometheus-node-exporter-ucode/files/extra/uci_dhcp_host.uc
new file mode 100644 (file)
index 0000000..0d55724
--- /dev/null
@@ -0,0 +1,14 @@
+import { cursor } from "uci";
+
+const uci = cursor();
+uci.load("dhcp");
+
+let m = gauge("dhcp_host_info");
+
+uci.foreach('dhcp', `host`, (s) => {
+       m({
+               name: s.name,
+               mac: s.mac,
+               ip: s.ip,
+       }, 1);
+});
diff --git a/utils/prometheus-node-exporter-ucode/files/extra/wifi.uc b/utils/prometheus-node-exporter-ucode/files/extra/wifi.uc
new file mode 100644 (file)
index 0000000..f5f58ef
--- /dev/null
@@ -0,0 +1,118 @@
+import { request, 'const' as wlconst } from 'nl80211';
+
+const x = ubus.call("network.wireless", "status");
+
+if (!x)
+       return false;
+
+const iftypes = [
+       "Unknown",
+       "Ad-Hoc",
+       "Client",
+       "Master",
+       "Master (VLAN)",
+       "WDS",
+       "Monitor",
+       "Mesh Point",
+       "P2P Client",
+       "P2P Go",
+       "P2P Device",
+       "OCB",
+];
+
+let m_radio_info = gauge("wifi_radio_info");
+let m_network_info = gauge("wifi_network_info");
+let m_network_quality = gauge("wifi_network_quality");
+let m_network_bitrate = gauge("wifi_network_bitrate");
+let m_network_noise = gauge("wifi_network_noise_dbm");
+let m_network_signal = gauge("wifi_network_signal_dbm");
+let m_stations_total = counter("wifi_stations_total");
+let m_station_inactive = gauge("wifi_station_inactive_milliseconds");
+let m_station_rx_bytes = counter("wifi_station_receive_bytes_total");
+let m_station_tx_bytes = counter("wifi_station_transmit_bytes_total");
+let m_station_rx_packets = counter("wifi_station_receive_packets_total");
+let m_station_tx_packets = counter("wifi_station_transmit_packets_total");
+let m_station_signal = gauge("wifi_station_signal_dbm");
+let m_station_rx_bitrate = gauge("wifi_station_receive_kilobits_per_second");
+let m_station_tx_bitrate = gauge("wifi_station_transmit_kilobits_per_second");
+let m_station_exp_tp = gauge("wifi_station_expected_throughput_kilobits_per_second");
+
+for (let radio in x) {
+       const rc = x[radio]["config"];
+
+       m_radio_info({
+               radio,
+               htmode: rc["htmode"],
+               channel: rc["channel"],
+               country: rc["country"],
+       } ,1);
+
+       for (let iface in x[radio]["interfaces"]) {
+               const ifname = iface["ifname"];
+               const nc = iface["config"];
+               const wif = request(wlconst.NL80211_CMD_GET_INTERFACE, 0, { dev: ifname });
+
+               if (!wif)
+                       continue;
+
+               m_network_info({
+                       radio,
+                       ifname,
+                       ssid: nc["ssid"] || nc["mesh_id"],
+                       bssid: wif["mac"],
+                       mode: iftypes[wif["iftype"]],
+               }, 1);
+
+               const wsta = request(wlconst.NL80211_CMD_GET_STATION, wlconst.NLM_F_DUMP, { dev: ifname });
+               let signal = 0;
+               let bitrate = 0;
+               const stations = length(wsta) || 0;
+               if (stations) {
+                       for (let sta in wsta) {
+                               signal += sta["sta_info"].signal;
+                               bitrate += sta["sta_info"]["tx_bitrate"].bitrate32;
+                       }
+                       bitrate /= stations * 0.01;
+                       signal /= stations;
+               }
+
+               let labels = { radio, ifname };
+               m_network_bitrate(labels, bitrate || NaN);
+               m_network_signal(labels, signal || NaN);
+               m_network_quality(labels, signal ? 100.0 / 70 * (signal + 110) : NaN);
+
+               const wsur = request(wlconst.NL80211_CMD_GET_SURVEY, wlconst.NLM_F_DUMP, { dev: ifname });
+               let noise = 0;
+               for (let i in wsur) {
+                       if (i["survey_info"]["frequency"] != wif["wiphy_freq"])
+                               continue;
+
+                       noise = i["survey_info"]["noise"];
+                       break;
+               }
+
+               m_network_noise(labels, noise || NaN);
+
+               if (config["stations"] != "1")
+                       continue;
+
+               m_stations_total(labels, stations);
+               if (!stations)
+                       continue;
+
+               for (let sta in wsta) {
+                       labels["mac"] = sta["mac"];
+                       const info = sta["sta_info"];
+
+                       m_station_inactive(labels, info["inactive_time"]);
+                       m_station_rx_bytes(labels, info["rx_bytes64"]);
+                       m_station_tx_bytes(labels, info["tx_bytes64"]);
+                       m_station_rx_packets(labels, info["rx_packets"]);
+                       m_station_tx_packets(labels, info["tx_packets"]);
+                       m_station_signal(labels, info["signal"]);
+                       m_station_rx_bitrate(labels, info["rx_bitrate"]["bitrate32"] * 100);
+                       m_station_tx_bitrate(labels, info["tx_bitrate"]["bitrate32"] * 100);
+                       m_station_exp_tp(labels, info["expected_throughput"]);
+               }
+       }
+}
diff --git a/utils/prometheus-node-exporter-ucode/files/extra/wireguard.uc b/utils/prometheus-node-exporter-ucode/files/extra/wireguard.uc
new file mode 100644 (file)
index 0000000..12ae56a
--- /dev/null
@@ -0,0 +1,49 @@
+import { cursor } from "uci";
+
+const x = ubus.call("wireguard", "status");
+if (!x)
+       return false;
+
+const uci = cursor();
+uci.load("network");
+
+let m_wg_iface_info = gauge("wireguard_interface_info");
+let m_wg_peer_info = gauge("wireguard_peer_info");
+let m_wg_handshake = gauge ("wireguard_latest_handshake_seconds");
+let m_wg_rx = gauge ("wireguard_received_bytes_total");
+let m_wg_tx = gauge ("wireguard_sent_bytes_total");
+
+for (let iface in x) {
+       const wc = x[iface];
+
+       m_wg_iface_info({
+               name: iface,
+               public_key: wc["public_key"],
+               listen_port: wc["listen_port"],
+               fwmark: wc["fwmark"] || NaN,
+       }, 1);
+
+       for (let peer in wc["peers"]) {
+               let description;
+               uci.foreach('network', `wireguard_${iface}`, (s) => {
+                       if (s.public_key == peer)
+                               description = s.description;
+               });
+
+               const pc = wc["peers"][peer];
+
+               m_wg_peer_info({
+                       interface: iface,
+                       public_key: peer,
+                       description,
+                       endpoint: pc["endpoint"],
+                       persistent_keepalive_interval: pc["persistent_keepalive_interval"] || NaN,
+               }, 1);
+
+               const labels = { public_key: peer };
+
+               m_wg_handshake(labels, pc["last_handshake"]);
+               m_wg_rx(labels, pc["rx_bytes"]);
+               m_wg_tx(labels, pc["tx_bytes"]);
+       }
+}
diff --git a/utils/prometheus-node-exporter-ucode/files/init b/utils/prometheus-node-exporter-ucode/files/init
new file mode 100644 (file)
index 0000000..1735236
--- /dev/null
@@ -0,0 +1,73 @@
+#!/bin/sh /etc/rc.common
+# Copyright (C) 2013-2017 OpenWrt.org
+
+START=60
+USE_PROCD=1
+
+_log() {
+       logger -p daemon.info -t prometheus-node-exporter-ucode "$@"
+}
+
+start_service() {
+       . /lib/functions/network.sh
+
+       local interface port bind4 bind6
+
+       config_load prometheus-node-exporter-ucode.main
+       config_get interface "main" listen_interface "loopback"
+       config_get port "main" listen_port 9101
+       config_get keepalive "main" http_keepalive 70
+
+       [ "$interface" = "*" ] || {
+               network_get_ipaddr  bind4 "$interface"
+               network_get_ipaddr6 bind6 "$interface"
+               [ -n "$bind4$bind6" ] || {
+                       _log "defering start until listen interface $interface becomes ready"
+                       return 0
+               }
+       }
+
+       procd_open_instance
+
+       procd_set_param command /usr/sbin/uhttpd -f -c /dev/null -h /dev/null -S -D -o /metrics -O /usr/share/ucode/node-exporter/metrics.uc
+
+       if [ "$interface" = "*" ]; then
+               procd_append_param command -p $port
+       else
+               [ -n "$bind4" ] && procd_append_param command -p $bind4:$port
+               [ -n "$bind6" ] && procd_append_param command -p [$bind6]:$port
+       fi
+       [ $keepalive -gt 0 ] && procd_append_param command -k $keepalive
+
+       procd_add_jail prometheus-node-exporter-ucode log procfs sysfs ubus
+       procd_add_jail_mount "/usr/lib/uhttpd_ucode.so"
+       procd_add_jail_mount "/lib/libubus.so*"
+       procd_add_jail_mount "/lib/libuci.so"
+       procd_add_jail_mount "/usr/lib/ucode"
+       procd_add_jail_mount "/usr/lib/libnl*.so*"
+       procd_add_jail_mount "/usr/share/ucode/node-exporter"
+       procd_add_jail_mount "/etc/config"
+
+       # TODO breaks the dsl collector?
+       #procd_set_param user nobody
+       #procd_set_param group nogroup
+       procd_set_param no_new_privs 1
+
+       procd_set_param stdout 1
+       procd_set_param stderr 1
+       procd_set_param respawn
+
+       procd_close_instance
+}
+
+service_triggers()
+{
+       local interface
+
+       procd_add_reload_trigger "prometheus-node-exporter-ucode"
+
+       config_load prometheus-node-exporter-ucode.main
+       config_get interface "main" listen_interface "loopback"
+
+       [ "$interface" = "*" ] || procd_add_reload_interface_trigger "$interface"
+}
diff --git a/utils/prometheus-node-exporter-ucode/files/metrics.uc b/utils/prometheus-node-exporter-ucode/files/metrics.uc
new file mode 100644 (file)
index 0000000..3dce77a
--- /dev/null
@@ -0,0 +1,227 @@
+{%
+'use strict';
+
+import * as fs from "fs";
+import { connect } from "ubus";
+import { cursor } from "uci";
+
+function debug(...s) {
+       if (global.debug)
+               warn("DEBUG: ", ...s, "\n");
+}
+
+function puts(...s) {
+       return uhttpd.send(...s, "\n");
+}
+
+function govalue(value) {
+       if (value == Infinity)
+               return "+Inf";
+       else if (value == -Infinity)
+               return "-Inf";
+       else if (value != value)
+               return "NaN";
+       else if (type(value) in [ "int", "double" ])
+               return value;
+       else if (type(value) in [ "bool", "string" ])
+               return +value;
+
+       return null;
+}
+
+function metric(name, mtype, help, skipdecl) {
+       let func;
+       let decl = skipdecl == true ? false : true;
+
+       let yield = function(labels, value) {
+               let v = govalue(value);
+
+               if (v == null) {
+                       debug(`skipping metric: unsupported value '${value}' (${name})`);
+                       return func;
+               }
+
+               let labels_str = "";
+               if (length(labels)) {
+                       let sep = "";
+                       let s;
+                       labels_str = "{";
+                       for (let l in labels) {
+                               if (labels[l] == null)
+                                       s = "";
+                               else if (type(labels[l]) == "string") {
+                                       s = labels[l];
+                                       s = replace(labels[l], "\\", "\\\\");
+                                       s = replace(s, "\"", "\\\"");
+                                       s = replace(s, "\n", "\\n");
+                               } else {
+                                       s = govalue(labels[l]);
+
+                                       if (!s)
+                                               continue;
+                               }
+
+                               labels_str += sep + l + "=\"" + s + "\"";
+                               sep = ",";
+                       }
+                       labels_str += "}";
+               }
+
+               if (decl) {
+                       if (help)
+                               puts("# HELP ", name, " ", help);
+                       puts("# TYPE ", name, " ", mtype);
+                       decl = false;
+               }
+
+               puts(name, labels_str, " ", v);
+               return func;
+       };
+
+       func = yield;
+       return func;
+}
+
+function counter(name, help, skipdecl) {
+       return metric(name, "counter", help, skipdecl);
+}
+
+function gauge(name, help, skipdecl) {
+       return metric(name, "gauge", help, skipdecl);
+}
+
+function httpstatus(status) {
+       puts("Status: ", status, "\nContent-Type: text/plain; version=0.0.4; charset=utf-8\n");
+}
+
+function clockdiff(t1, t2) {
+       return (t2[0] - t1[0]) * 1000000000 + t2[1] - t1[1];
+}
+
+let collectors = {};
+
+global.handle_request = function(env) {
+       let scope = {
+               config: null,
+               fs,
+               ubus: connect(),
+               counter,
+               gauge,
+               wsplit: function(line) {
+                       return split(line, /\s+/);
+               },
+               nextline: function(f) {
+                       return rtrim(f.read("line"), "\n");
+               },
+               oneline: function(fn) {
+                       let f = fs.open(fn);
+
+                       if (!f)
+                               return null;
+
+                       return nextline(f);
+               },
+               poneline: function(cmd) {
+                       let f = fs.popen(cmd);
+
+                       if (!f)
+                               return null;
+
+                       return nextline(f);
+               },
+       };
+
+       if (length(collectors) < 1) {
+               httpstatus("404 No Collectors found");
+               return;
+       }
+
+       let cols = [];
+       for (let q in split(env.QUERY_STRING, "&")) {
+               let s = split(q, "=", 2);
+               if (length(s) == 2 && s[0] == "collect") {
+                       if (!(s[1] in collectors)) {
+                               httpstatus(`404 Collector ${s[1]} not found`);
+                               return;
+                       }
+
+                       push(cols, s[1]);
+               }
+       }
+
+       if (length(cols) > 0)
+               cols = uniq(cols);
+       else
+               cols = keys(collectors);
+
+       httpstatus("200 OK");
+
+       let duration = gauge("node_scrape_collector_duration_seconds");
+       let success = gauge("node_scrape_collector_success");
+
+       for (let col in cols) {
+               let ok = false;
+               let t1, t2;
+
+               scope["config"] = collectors[col].config;
+               t1 = clock(true);
+               try {
+                       ok = call(collectors[col].func, null, scope) != false;
+               } catch(e) {
+                       warn(`error running collector '${col}':\n${e.message}\n`);
+               }
+               t2 = clock(true);
+
+               duration({ collector: col }, clockdiff(t1, t2) / 1000000000.0);
+               success({ collector: col }, ok);
+       }
+};
+
+const lib = "/usr/share/ucode/node-exporter/lib";
+const opts = {
+       strict_declarations:    true,
+       raw_mode:               true,
+};
+
+let cols = fs.lsdir(lib, "*.uc");
+for (let col in cols) {
+       let func;
+       let uci = cursor();
+
+       try {
+               func = loadfile(lib + "/" + col, opts);
+       } catch(e) {
+               warn(`error compiling collector '${col}':\n${e.message}\n`);
+               continue;
+       }
+
+       let name = substr(col, 0, -3);
+       let config = uci.get_all("prometheus-node-exporter-ucode", name);
+       if (!config || config[".type"] != "collector")
+               config = {};
+       else {
+               delete config[".anonymous"];
+               delete config[".type"];
+               delete config[".name"];
+       }
+
+       collectors[name] = {
+               func,
+               config,
+       };
+}
+
+warn(`prometheus-node-exporter-ucode now serving requests with ${length(collectors)} collectors\n`);
+
+if (!("uhttpd" in global)) {
+       global.debug = true;
+
+       puts = function(...s) {
+               return print(...s, "\n");
+       };
+
+       handle_request({
+               QUERY_STRING: join("&", map(ARGV, v => "collect=" + v)),
+       });
+}
+%}
diff --git a/utils/prometheus-node-exporter-ucode/files/run.sh b/utils/prometheus-node-exporter-ucode/files/run.sh
new file mode 100755 (executable)
index 0000000..a7cefef
--- /dev/null
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec /usr/bin/ucode -T /usr/share/ucode/node-exporter/metrics.uc $*
diff --git a/utils/prometheus-node-exporter-ucode/test.sh b/utils/prometheus-node-exporter-ucode/test.sh
new file mode 100755 (executable)
index 0000000..e72942b
--- /dev/null
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+prometheus-node-exporter-ucode time