prometheus-node-exporter-lua: adds node metrics exporter 4331/head
authorChristian Simon <simon@swine.de>
Sat, 6 May 2017 17:18:10 +0000 (18:18 +0100)
committerChristian Simon <simon@swine.de>
Mon, 8 May 2017 13:27:43 +0000 (14:27 +0100)
Signed-off-by: Christian Simon <simon@swine.de>
utils/prometheus-node-exporter-lua/Makefile [new file with mode: 0644]
utils/prometheus-node-exporter-lua/files/etc/config/prometheus-node-exporter-lua [new file with mode: 0644]
utils/prometheus-node-exporter-lua/files/etc/init.d/prometheus-node-exporter-lua [new file with mode: 0644]
utils/prometheus-node-exporter-lua/files/usr/bin/prometheus-node-exporter-lua [new file with mode: 0755]

diff --git a/utils/prometheus-node-exporter-lua/Makefile b/utils/prometheus-node-exporter-lua/Makefile
new file mode 100644 (file)
index 0000000..6662eb0
--- /dev/null
@@ -0,0 +1,45 @@
+#
+# Copyright (C) 2013-2017 OpenWrt.org
+#
+include $(TOPDIR)/rules.mk
+
+PKG_NAME:=prometheus-node-exporter-lua
+PKG_VERSION:=2017.05.07
+PKG_RELEASE:=1
+
+PKG_MAINTAINER:=Christian Simon <simon@swine.de>
+PKG_LICENSE:=Apache-2.0
+
+include $(INCLUDE_DIR)/package.mk
+
+define Package/prometheus-node-exporter-lua
+  SECTION:=utils
+  CATEGORY:=Utilities
+  TITLE:=Provides system statistics as Prometheus scraping endpoint
+  DEPENDS:=+luasocket
+  URL:=https://github.com/rbo/openwrt_exporter
+  PKGARCH:=all
+endef
+
+define Package/prometheus-node-exporter-lua/conffiles
+/etc/config/prometheus-node-exporter-lua
+endef
+
+define Package/prometheus-node-exporter-lua/description
+  Provides node metrics as Prometheus scraping endpoint.
+
+  This service is a lightweight rewrite in LUA of the offical Prometheus node_exporter.
+endef
+
+Build/Compile=
+
+define Package/prometheus-node-exporter-lua/install
+       $(INSTALL_DIR) $(1)/etc/config
+       $(INSTALL_CONF) ./files/etc/config/prometheus-node-exporter-lua $(1)/etc/config/prometheus-node-exporter-lua
+       $(INSTALL_DIR) $(1)/etc/init.d
+       $(INSTALL_BIN) ./files/etc/init.d/prometheus-node-exporter-lua $(1)/etc/init.d/prometheus-node-exporter-lua
+       $(INSTALL_DIR) $(1)/usr/bin
+       $(INSTALL_BIN) ./files/usr/bin/prometheus-node-exporter-lua $(1)/usr/bin/prometheus-node-exporter-lua
+endef
+
+$(eval $(call BuildPackage,prometheus-node-exporter-lua))
diff --git a/utils/prometheus-node-exporter-lua/files/etc/config/prometheus-node-exporter-lua b/utils/prometheus-node-exporter-lua/files/etc/config/prometheus-node-exporter-lua
new file mode 100644 (file)
index 0000000..1401a1a
--- /dev/null
@@ -0,0 +1,3 @@
+config prometheus-node-exporter-lua 'main'
+       option listen_address '::1'
+       option listen_port '9100'
diff --git a/utils/prometheus-node-exporter-lua/files/etc/init.d/prometheus-node-exporter-lua b/utils/prometheus-node-exporter-lua/files/etc/init.d/prometheus-node-exporter-lua
new file mode 100644 (file)
index 0000000..fc8a0b2
--- /dev/null
@@ -0,0 +1,19 @@
+#!/bin/sh /etc/rc.common
+# Copyright (C) 2013-2017 OpenWrt.org
+
+START=60
+USE_PROCD=1
+
+start_service() {
+       procd_open_instance
+
+       config_load prometheus-node-exporter-lua.main
+       config_get bind "main" listen_address ::1
+       config_get port "main" listen_port 9100
+
+       procd_set_param command /usr/bin/prometheus-node-exporter-lua
+       procd_append_param command --port ${port}
+       procd_append_param command --bind ${bind}
+
+       procd_close_instance
+}
diff --git a/utils/prometheus-node-exporter-lua/files/usr/bin/prometheus-node-exporter-lua b/utils/prometheus-node-exporter-lua/files/usr/bin/prometheus-node-exporter-lua
new file mode 100755 (executable)
index 0000000..fea8469
--- /dev/null
@@ -0,0 +1,391 @@
+#!/usr/bin/lua
+
+-- Metrics web server
+
+-- Copyright (c) 2016 Jeff Schornick <jeff@schornick.org>
+-- Copyright (c) 2015 Kevin Lyda
+-- Licensed under the Apache License, Version 2.0
+
+socket = require("socket")
+
+-- Allow us to call unpack under both lua5.1 and lua5.2+
+local unpack = unpack or table.unpack
+
+-- This table defines the scrapers to run.
+-- Each corresponds directly to a scraper_<name> function.
+scrapers = { "cpu", "load_averages", "memory", "file_handles", "network",
+             "network_devices", "time", "uname", "nat", "wifi"}
+
+-- Parsing
+
+function space_split(s)
+  elements = {}
+  for element in s:gmatch("%S+") do
+    table.insert(elements, element)
+  end
+  return elements
+end
+
+function line_split(s)
+  elements = {}
+  for element in s:gmatch("[^\n]+") do
+    table.insert(elements, element)
+  end
+  return elements
+end
+
+function get_contents(filename)
+  local f = io.open(filename, "rb")
+  local contents = ""
+  if f then
+    contents = f:read "*a"
+    f:close()
+  end
+
+  return contents
+end
+
+-- Metric printing
+
+function print_metric(metric, labels, value)
+  local label_string = ""
+  if labels then
+    for label,value in pairs(labels) do
+      label_string =  label_string .. label .. '="' .. value .. '",'
+    end
+    label_string = "{" .. string.sub(label_string, 1, -2) .. "}"
+  end
+  output(string.format("%s%s %s", metric, label_string, value))
+end
+
+function metric(name, mtype, labels, value)
+  output("# TYPE " .. name .. " " .. mtype)
+  local outputter = function(labels, value)
+    print_metric(name, labels, value)
+  end
+  if value then
+    outputter(labels, value)
+  end
+  return outputter
+end
+
+function scraper_wifi()
+  local rv = { }
+  local ntm = require "luci.model.network".init()
+
+  local metric_wifi_network_up = metric("wifi_network_up","gauge")
+  local metric_wifi_network_quality = metric("wifi_network_quality","gauge")
+  local metric_wifi_network_bitrate = metric("wifi_network_bitrate","gauge")
+  local metric_wifi_network_noise = metric("wifi_network_noise","gauge")
+  local metric_wifi_network_signal = metric("wifi_network_signal","gauge")
+
+  local metric_wifi_station_signal = metric("wifi_station_signal","gauge")
+  local metric_wifi_station_tx_packets = metric("wifi_station_tx_packets","gauge")
+  local metric_wifi_station_rx_packets = metric("wifi_station_rx_packets","gauge")
+
+  local dev
+  for _, dev in ipairs(ntm:get_wifidevs()) do
+    local rd = {
+      up       = dev:is_up(),
+      device   = dev:name(),
+      name     = dev:get_i18n(),
+      networks = { }
+    }
+
+    local net
+    for _, net in ipairs(dev:get_wifinets()) do
+      local labels = {
+        channel = net:channel(),
+        ssid = net:active_ssid(),
+        bssid = net:active_bssid(),
+        mode = net:active_mode(),
+        ifname = net:ifname(),
+        country = net:country(),
+        frequency = net:frequency(),
+      }
+      if net:is_up() then
+        metric_wifi_network_up(labels, 1)
+        local signal = net:signal_percent()
+        if signal ~= 0 then
+          metric_wifi_network_quality(labels, net:signal_percent())
+        end
+       metric_wifi_network_noise(labels, net:noise())
+       local bitrate = net:bitrate()
+       if bitrate then
+          metric_wifi_network_bitrate(labels, bitrate)
+       end
+
+        local assoclist = net:assoclist()
+        for mac, station in pairs(assoclist) do
+          local labels = {
+            ifname = net:ifname(),
+            mac = mac,
+          }
+          metric_wifi_station_signal(labels, station.signal)
+          metric_wifi_station_tx_packets(labels, station.tx_packets)
+          metric_wifi_station_rx_packets(labels, station.rx_packets)
+        end
+      else
+        metric_wifi_network_up(labels, 0)
+      end
+    end
+    rv[#rv+1] = rd
+  end
+end
+
+function scraper_cpu()
+  local stat = get_contents("/proc/stat")
+
+  -- system boot time, seconds since epoch
+  metric("node_boot_time", "gauge", nil, string.match(stat, "btime ([0-9]+)"))
+
+  -- context switches since boot (all CPUs)
+  metric("node_context_switches", "counter", nil, string.match(stat, "ctxt ([0-9]+)"))
+
+  -- cpu times, per CPU, per mode
+  local cpu_mode = {"user", "nice", "system", "idle", "iowait", "irq",
+                    "softirq", "steal", "guest", "guest_nice"}
+  local i = 0
+  local cpu_metric = metric("node_cpu", "counter")
+  while string.match(stat, string.format("cpu%d ", i)) do
+    local cpu = space_split(string.match(stat, string.format("cpu%d ([0-9 ]+)", i)))
+    local labels = {cpu = "cpu" .. i}
+    for ii, mode in ipairs(cpu_mode) do
+      labels['mode'] = mode
+      cpu_metric(labels, cpu[ii] / 100)
+    end
+    i = i + 1
+  end
+
+  -- interrupts served
+  metric("node_intr", "counter", nil, string.match(stat, "intr ([0-9]+)"))
+
+  -- processes forked
+  metric("node_forks", "counter", nil, string.match(stat, "processes ([0-9]+)"))
+
+  -- processes running
+  metric("node_procs_running", "gauge", nil, string.match(stat, "procs_running ([0-9]+)"))
+
+  -- processes blocked for I/O
+  metric("node_procs_blocked", "gauge", nil, string.match(stat, "procs_blocked ([0-9]+)"))
+end
+
+function scraper_load_averages()
+  local loadavg = space_split(get_contents("/proc/loadavg"))
+
+  metric("node_load1", "gauge", nil, loadavg[1])
+  metric("node_load5", "gauge", nil, loadavg[2])
+  metric("node_load15", "gauge", nil, loadavg[3])
+end
+
+function scraper_memory()
+  local meminfo = line_split(get_contents("/proc/meminfo"):gsub("[):]", ""):gsub("[(]", "_"))
+
+  for i, mi in ipairs(meminfo) do
+    local name, size, unit = unpack(space_split(mi))
+    if unit == 'kB' then
+      size = size * 1024
+    end
+    metric("node_memory_" .. name, "gauge", nil, size)
+  end
+end
+
+function scraper_file_handles()
+  local file_nr = space_split(get_contents("/proc/sys/fs/file-nr"))
+
+  metric("node_filefd_allocated", "gauge", nil, file_nr[1])
+  metric("node_filefd_maximum", "gauge", nil, file_nr[3])
+end
+
+function scraper_network()
+  -- NOTE: Both of these are missing in OpenWRT kernels.
+  --       See: https://dev.openwrt.org/ticket/15781
+  local netstat = get_contents("/proc/net/netstat") .. get_contents("/proc/net/snmp")
+
+  -- all devices
+  local netsubstat = {"IcmpMsg", "Icmp", "IpExt", "Ip", "TcpExt", "Tcp", "UdpLite", "Udp"}
+  for i, nss in ipairs(netsubstat) do
+    local substat_s = string.match(netstat, nss .. ": ([A-Z][A-Za-z0-9 ]+)")
+    if substat_s then
+      local substat = space_split(substat_s)
+      local substatv = space_split(string.match(netstat, nss .. ": ([0-9 -]+)"))
+      for ii, ss in ipairs(substat) do
+        metric("node_netstat_" .. nss .. "_" .. ss, "gauge", nil, substatv[ii])
+      end
+    end
+  end
+end
+
+function scraper_network_devices()
+  local netdevstat = line_split(get_contents("/proc/net/dev"))
+  local netdevsubstat = {"receive_bytes", "receive_packets", "receive_errs",
+                   "receive_drop", "receive_fifo", "receive_frame", "receive_compressed",
+                   "receive_multicast", "transmit_bytes", "transmit_packets",
+                   "transmit_errs", "transmit_drop", "transmit_fifo", "transmit_colls",
+                   "transmit_carrier", "transmit_compressed"}
+  for i, line in ipairs(netdevstat) do
+    netdevstat[i] = string.match(netdevstat[i], "%S.*")
+  end
+  local nds_table = {}
+  local devs = {}
+  for i, nds in ipairs(netdevstat) do
+    local dev, stat_s = string.match(netdevstat[i], "([^:]+): (.*)")
+    if dev then
+      nds_table[dev] = space_split(stat_s)
+      table.insert(devs, dev)
+    end
+  end
+  for i, ndss in ipairs(netdevsubstat) do
+    netdev_metric = metric("node_network_" .. ndss, "gauge")
+    for ii, d in ipairs(devs) do
+      netdev_metric({device=d}, nds_table[d][i])
+    end
+  end
+end
+
+function scraper_time()
+  -- current time
+  metric("node_time", "counter", nil, os.time())
+end
+
+function scraper_uname()
+  -- version can have spaces, so grab it directly
+  local version = string.sub(io.popen("uname -v"):read("*a"), 1, -2)
+  -- avoid individual popen calls for the rest of the values
+  local uname_string = io.popen("uname -a"):read("*a")
+  local sysname, nodename, release = unpack(space_split(uname_string))
+  local labels = {domainname = "(none)", nodename = nodename, release = release,
+                  sysname = sysname, version = version}
+
+  -- The machine hardware name is immediately after the version string, so add
+  -- up the values we know and add in the 4 spaces to find the offset...
+  machine_offset = string.len(sysname .. nodename .. release .. version) + 4
+  labels['machine'] = string.match(string.sub(uname_string, machine_offset), "(%S+)" )
+  metric("node_uname_info", "gauge", labels, 1)
+end
+
+function scraper_nat()
+  -- documetation about nf_conntrack:
+  -- https://www.frozentux.net/iptables-tutorial/chunkyhtml/x1309.html
+  -- local natstat = line_split(get_contents("/proc/net/nf_conntrack"))
+  local natstat = line_split(get_contents("nf_conntrack"))
+
+  nat_metric =  metric("node_nat_traffic", "gauge" )
+  for i, e in ipairs(natstat) do
+    -- output(string.format("%s\n",e  ))
+    local fields = space_split(e)
+    local src, dest, bytes;
+    bytes = 0;
+    for ii, field in ipairs(fields) do
+      if src == nil and string.match(field, '^src') then
+        src = string.match(field,"src=([^ ]+)");
+      elseif dest == nil and string.match(field, '^dst') then
+        dest = string.match(field,"dst=([^ ]+)");
+      elseif string.match(field, '^bytes') then
+        local b = string.match(field, "bytes=([^ ]+)");
+        bytes = bytes + b;
+        -- output(string.format("\t%d %s",ii,field  ));
+      end
+
+    end
+    -- local src, dest, bytes = string.match(natstat[i], "src=([^ ]+) dst=([^ ]+) .- bytes=([^ ]+)");
+    -- local src, dest, bytes = string.match(natstat[i], "src=([^ ]+) dst=([^ ]+) sport=[^ ]+ dport=[^ ]+ packets=[^ ]+ bytes=([^ ]+)")
+
+    local labels = { src = src, dest = dest }
+    -- output(string.format("src=|%s| dest=|%s| bytes=|%s|", src, dest, bytes  ))
+    nat_metric(labels, bytes )
+  end
+end
+
+function timed_scrape(scraper)
+  local start_time = socket.gettime()
+  -- build the function name and call it from global variable table
+  _G["scraper_"..scraper]()
+  local duration = socket.gettime() - start_time
+  return duration
+end
+
+function run_all_scrapers()
+  times = {}
+  for i,scraper in ipairs(scrapers) do
+    runtime = timed_scrape(scraper)
+    times[scraper] = runtime
+    scrape_time_sums[scraper] = scrape_time_sums[scraper] + runtime
+    scrape_counts[scraper] = scrape_counts[scraper] + 1
+  end
+
+  local name = "node_exporter_scrape_duration_seconds"
+  local duration_metric = metric(name, "summary")
+  for i,scraper in ipairs(scrapers) do
+    local labels = {collector=scraper, result="success"}
+    duration_metric(labels, times[scraper])
+    print_metric(name.."_sum", labels, scrape_time_sums[scraper])
+    print_metric(name.."_count", labels, scrape_counts[scraper])
+  end
+end
+
+-- Web server-specific functions
+
+function http_ok_header()
+  output("HTTP/1.1 200 OK\r")
+  output("Server: lua-metrics\r")
+  output("Content-Type: text/plain; version=0.0.4\r")
+  output("\r")
+end
+
+function http_not_found()
+  output("HTTP/1.1 404 Not Found\r")
+  output("Server: lua-metrics\r")
+  output("Content-Type: text/plain\r")
+  output("\r")
+  output("ERROR: File Not Found.")
+end
+
+function serve(request)
+  if not string.match(request, "GET /metrics.*") then
+    http_not_found()
+  else
+    http_ok_header()
+    run_all_scrapers()
+  end
+  client:close()
+  return true
+end
+
+-- Main program
+
+for k,v in ipairs(arg) do
+  if (v == "-p") or (v == "--port") then
+    port = arg[k+1]
+  end
+  if (v == "-b") or (v == "--bind") then
+    bind = arg[k+1]
+  end
+end
+
+scrape_counts = {}
+scrape_time_sums = {}
+for i,scraper in ipairs(scrapers) do
+  scrape_counts[scraper] = 0
+  scrape_time_sums[scraper] = 0
+end
+
+if port then
+  server = assert(socket.bind(bind, port))
+
+  while 1 do
+    client = server:accept()
+    client:settimeout(60)
+    local request, err = client:receive()
+
+    if not err then
+      output = function (str) client:send(str.."\n") end
+      if not serve(request) then
+        break
+      end
+    end
+  end
+else
+  output = print
+  run_all_scrapers()
+end