This is a port of prometheus-node-exporter-lua to ucode.
Signed-off-by: Andre Heider <a.heider@gmail.com>
--- /dev/null
+# 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))
--- /dev/null
+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"));
--- /dev/null
+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]);
+}
--- /dev/null
+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"));
--- /dev/null
+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]);
--- /dev/null
+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]);
--- /dev/null
+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]);
+}
--- /dev/null
+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);
+}
--- /dev/null
+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]);
+}
--- /dev/null
+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);
--- /dev/null
+gauge("node_time_seconds")(null, time());
--- /dev/null
+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);
--- /dev/null
+config prometheus-node-exporter-ucode 'main'
+ option listen_interface 'loopback'
+ option listen_port '9101'
+ option http_keepalive '70'
+
+config collector 'wifi'
+ option stations '1'
--- /dev/null
+const x = ubus.call("dnsmasq", "metrics");
+if (!x)
+ return false;
+
+for (let i in x)
+ gauge(`dnsmasq_${i}_total`)(null, x[i]);
--- /dev/null
+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);
--- /dev/null
+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;
--- /dev/null
+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);
--- /dev/null
+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);
--- /dev/null
+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);
+});
--- /dev/null
+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"]);
+ }
+ }
+}
--- /dev/null
+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"]);
+ }
+}
--- /dev/null
+#!/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"
+}
--- /dev/null
+{%
+'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)),
+ });
+}
+%}
--- /dev/null
+#!/bin/sh
+exec /usr/bin/ucode -T /usr/share/ucode/node-exporter/metrics.uc $*
--- /dev/null
+#!/bin/sh
+
+prometheus-node-exporter-ucode time