|
@ -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 |