#!/usr/bin/lua -- Metrics web server -- Copyright (c) 2016 Jeff Schornick -- 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_ 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