#!/usr/bin/env lua -- Prometheus node metrics for BusyBox httpd, compatible with Lua 5.1+. -- Linux data comes directly from /proc and /sys. Filesystem statistics use -- statvfs when a Lua binding (nixio or luaposix) is installed, otherwise a -- single shell invocation running df twice (bytes and inodes). -- -- Performance notes (this file is optimised for CPU time and heap size): -- * Metric lines are written as several io.write arguments straight into -- the stdio buffer. Nothing is string.format'ed or concatenated per line, -- so the output path allocates no Lua strings at all. -- * /proc lines are parsed with a single string.match whose captures land -- in locals; no per-line token tables are built. -- * Escaping and normalisation helpers test with string.find first and only -- call gsub when the value actually contains a special character. -- * /proc/stat is read once; btime is remembered for process_start_time. pcall(os.setlocale, "C") local format, match, find, gmatch, gsub, byte, sub = string.format, string.match, string.find, string.gmatch, string.gsub, string.byte, string.sub local concat = table.concat local tonumber, tostring, type, getenv, pcall = tonumber, tostring, type, os.getenv, pcall local floor = math.floor local write = io.write --------------------------------------------------------------------------- -- Output buffer --------------------------------------------------------------------------- -- Fully buffered stdout. (glibc/musl size the buffer themselves; the hint is -- harmless where it is ignored.) io.stdout:setvbuf("full", 65536) local stdout = io.stdout local function flush() stdout:flush() end local gateway = getenv("GATEWAY_INTERFACE") or "" if find(gateway, "^CGI/") then write("Content-Type: text/plain; version=0.0.4; charset=utf-8\r\n", "Cache-Control: no-store\r\nX-Content-Type-Options: nosniff\r\n\r\n") if (getenv("REQUEST_METHOD") or "GET") == "HEAD" then flush() os.exit(0) end end --------------------------------------------------------------------------- -- Helpers --------------------------------------------------------------------------- local function read_line(path) local f = io.open(path, "r") if not f then return nil end local value = f:read("*l") f:close() return value end local function read_all(path) local f = io.open(path, "r") if not f then return nil end local value = f:read("*a") f:close() return value end local function each_line(path, callback) local f = io.open(path, "r") if not f then return false end for line in f:lines() do callback(line) end f:close() return true end local function label(value) if value == nil then return "" end if type(value) ~= "string" then value = tostring(value) end if not find(value, '[\\"\n]') then return value end value = gsub(value, "\\", "\\\\") value = gsub(value, "\n", "\\n") value = gsub(value, '"', '\\"') return value end -- name{labels} value local function metric(name, value, labels) if value == nil then return end if type(value) == "number" then -- Render numbers here instead of letting io.write pick LUAI_NUMFMT, -- which turns a large counter into "1.3721671085e+10". "%.0f" also -- stays exact on the Lua 5.1 builds that refuse "%d" for a double -- (OpenWrt's, which adds an int32 integer type). value = value == floor(value) and format("%.0f", value) or format("%.14g", value) end write(name, labels or "", " ", value, "\n") end local function metric_integer(name, value, labels) value = tonumber(value) if not value then return end write(name, labels or "", " ", format("%.0f", value), "\n") end local function metric_fraction(name, value, divisor, labels) value = tonumber(value) if not value then return end write(name, labels or "", " ", format("%.6f", value / divisor), "\n") end local function help(name, text, kind) write("# HELP ", name, " ", text, "\n# TYPE ", name, " ", kind, "\n") end local function uptime() local line = read_line("/proc/uptime") return line and tonumber(match(line, "^(%S+)")) or nil end local scrape_started = uptime() or os.clock() local clk_tck = tonumber(getenv("NODE_EXPORTER_CLK_TCK") or "100") or 100 if clk_tck <= 0 then clk_tck = 100 end local function architecture() local arch = getenv("HOSTTYPE") or getenv("MACHTYPE") if arch and arch ~= "" then return match(arch, "^[^-]+") or arch end arch = match(package and package.cpath or "", "/lib/([^/]+)/lua") if arch then return match(arch, "^[^-]+") or arch end -- Only the first processor block of /proc/cpuinfo is needed; stop early -- instead of reading the whole file (which is large on many-core hosts). local f = io.open("/proc/cpuinfo", "r") if not f then return "unknown" end arch = nil for line in f:lines() do if line == "" then break end if find(line, "^vendor_id") then arch = "x86_64" break end local version = match(line, "^CPU architecture%s*:%s*(%d+)") if version then arch = version == "8" and "aarch64" or ("armv" .. version .. "l") break end if find(line, "^cpu model%s*:%s*MIPS") then arch = "mips" break end if find(line, "^isa%s*:%s*rv64") then arch = "riscv64" break end end f:close() return arch or "unknown" end --------------------------------------------------------------------------- -- Collectors --------------------------------------------------------------------------- local function collect_uname_os() help("node_uname_info", "Labeled system information as provided by the kernel.", "gauge") write(format('node_uname_info{domainname="%s",machine="%s",nodename="%s",release="%s",sysname="%s",version="%s"} 1\n', label(read_line("/proc/sys/kernel/domainname")), label(architecture()), label(read_line("/proc/sys/kernel/hostname")), label(read_line("/proc/sys/kernel/osrelease")), label(read_line("/proc/sys/kernel/ostype")), label(read_line("/proc/sys/kernel/version")))) local osinfo = {} each_line("/etc/os-release", function(line) local key, value = match(line, "^([A-Z][A-Z0-9_]*)=(.*)$") if key then if find(value, '^".*"$') then value = sub(value, 2, -2) if find(value, "\\", 1, true) then value = gsub(value, '\\"', '"') value = gsub(value, "\\\\", "\\") end end osinfo[key] = value end end) help("node_os_info", "Labeled operating system information from /etc/os-release.", "gauge") write(format('node_os_info{build_id="%s",id="%s",image_id="%s",image_version="%s",name="%s",pretty_name="%s",variant="%s",variant_id="%s",version_codename="%s",version_id="%s"} 1\n', label(osinfo.BUILD_ID), label(osinfo.ID), label(osinfo.IMAGE_ID), label(osinfo.IMAGE_VERSION), label(osinfo.NAME), label(osinfo.PRETTY_NAME), label(osinfo.VARIANT), label(osinfo.VARIANT_ID), label(osinfo.VERSION_CODENAME), label(osinfo.VERSION_ID))) end local cpu_ids = {} local boot_time -- btime from /proc/stat, reused by collect_self local stat_metrics = { btime = "node_boot_time_seconds", ctxt = "node_context_switches_total", processes = "node_forks_total", intr = "node_intr_total", procs_running = "node_procs_running", procs_blocked = "node_procs_blocked", } -- node_cpu_seconds_total{cpu="N",mode="M"} value, written as pieces local function cpu_seconds(head, cpu, tail, jiffies) write(head, cpu, tail, format("%.6f", jiffies / clk_tck), "\n") end local function collect_stat() help("node_boot_time_seconds", "Node boot time, in unixtime.", "gauge") help("node_time_seconds", "System time in seconds since epoch.", "gauge") metric("node_time_seconds", os.time()) help("node_cpu_seconds_total", "Seconds the CPUs spent in each mode.", "counter") help("node_cpu_guest_seconds_total", "Seconds the CPUs spent in guest mode.", "counter") help("node_context_switches_total", "Total number of context switches.", "counter") help("node_forks_total", "Total number of forks.", "counter") help("node_intr_total", "Total number of interrupts serviced.", "counter") help("node_procs_running", "Number of processes in runnable state.", "gauge") help("node_procs_blocked", "Number of processes blocked waiting for I/O.", "gauge") local f = io.open("/proc/stat", "r") if not f then return end for line in f:lines() do -- Optional trailing fields (steal/guest/guest_nice) are absent on old kernels. local cpu, user, nice, system, idle, iowait, irq, softirq, steal, guest, guest_nice = match(line, "^cpu(%d+)%s+(%d+)%s*(%d*)%s*(%d*)%s*(%d*)%s*(%d*)%s*(%d*)%s*(%d*)%s*(%d*)%s*(%d*)%s*(%d*)") if cpu then cpu_ids[#cpu_ids + 1] = cpu guest, guest_nice = tonumber(guest) or 0, tonumber(guest_nice) or 0 cpu_seconds('node_cpu_seconds_total{cpu="', cpu, '",mode="user"} ', (tonumber(user) or 0) - guest) cpu_seconds('node_cpu_seconds_total{cpu="', cpu, '",mode="nice"} ', (tonumber(nice) or 0) - guest_nice) cpu_seconds('node_cpu_seconds_total{cpu="', cpu, '",mode="system"} ', tonumber(system) or 0) cpu_seconds('node_cpu_seconds_total{cpu="', cpu, '",mode="idle"} ', tonumber(idle) or 0) cpu_seconds('node_cpu_seconds_total{cpu="', cpu, '",mode="iowait"} ', tonumber(iowait) or 0) cpu_seconds('node_cpu_seconds_total{cpu="', cpu, '",mode="irq"} ', tonumber(irq) or 0) cpu_seconds('node_cpu_seconds_total{cpu="', cpu, '",mode="softirq"} ', tonumber(softirq) or 0) cpu_seconds('node_cpu_seconds_total{cpu="', cpu, '",mode="steal"} ', tonumber(steal) or 0) cpu_seconds('node_cpu_guest_seconds_total{cpu="', cpu, '",mode="user"} ', guest) cpu_seconds('node_cpu_guest_seconds_total{cpu="', cpu, '",mode="nice"} ', guest_nice) else local key, value = match(line, "^(%S+)%s+(%S+)") local name = key and stat_metrics[key] if name then if key == "btime" then boot_time = value end metric(name, value) end end end f:close() end local function collect_loadavg() local line = read_line("/proc/loadavg") if not line then return end local load1, load5, load15, running, total = match(line, "^(%S+)%s+(%S+)%s+(%S+)%s+(%d+)/(%d+)") if not load1 then return end help("node_load1", "1m load average.", "gauge"); metric("node_load1", load1) help("node_load5", "5m load average.", "gauge"); metric("node_load5", load5) help("node_load15", "15m load average.", "gauge"); metric("node_load15", load15) help("node_loadavg_processes_running", "Number of processes in runnable state.", "gauge") metric("node_loadavg_processes_running", running) help("node_loadavg_processes_total", "Total number of processes.", "gauge") metric("node_loadavg_processes_total", total) end local function collect_meminfo() local f = io.open("/proc/meminfo", "r") if not f then return end for line in f:lines() do local key, value, unit = match(line, "^([^:]+):%s+(%-?%d+)%s*(%S*)") if key then if find(key, "[%(%)%-]") then key = gsub(gsub(gsub(key, "%(", "_"), "%)", ""), "%-", "_") end local suffix = "" if unit == "kB" then suffix, value = "_bytes", format("%.0f", (tonumber(value) or 0) * 1024) end write("# HELP node_memory_", key, suffix, " Memory information field ", key, ".\n# TYPE node_memory_", key, suffix, " gauge\nnode_memory_", key, suffix, " ", value, "\n") end end f:close() end local function collect_vmstat() local f = io.open("/proc/vmstat", "r") if not f then return end -- Lines are "key value" with one space, so the validated line is written -- as-is instead of capturing (and allocating) the two fields. for line in f:lines() do if find(line, "^[%w_]+ %-?%d+$") then write("node_vmstat_", line, "\n") end end f:close() end local function disk_excluded(dev) local last = byte(dev, -1) if not last or last < 48 or last > 57 then return false end -- every excluded pattern ends in a digit return find(dev, "^ram%d+$") or find(dev, "^loop%d+$") or find(dev, "^fd%d+$") or find(dev, "^x?[shv]d%a+%d+$") -- sdXN, hdXN, vdXN, xvdXN partitions or find(dev, "^nvme%d+n%d+p%d+$") or find(dev, "^mmcblk%d+p%d+$") end -- major minor device + 11 mandatory fields (kernel >= 2.6) + optional discard (4.18) and flush (5.5) fields local diskstats_pattern = "^%s*%d+%s+%d+%s+%S+%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)" .. "%s*(%d*)%s*(%d*)%s*(%d*)%s*(%d*)%s*(%d*)%s*(%d*)" local function disk_metrics(labels, line) local reads, reads_merged, read_sectors, read_ms, writes, writes_merged, written_sectors, write_ms, io_now, io_ms, io_weighted_ms, discards, discards_merged, discarded_sectors, discard_ms, flushes, flush_ms = match(line, diskstats_pattern) if not reads then return end metric("node_disk_reads_completed_total", reads, labels) metric("node_disk_reads_merged_total", reads_merged, labels) metric_integer("node_disk_read_bytes_total", (tonumber(read_sectors) or 0) * 512, labels) metric_fraction("node_disk_read_time_seconds_total", read_ms, 1000, labels) metric("node_disk_writes_completed_total", writes, labels) metric("node_disk_writes_merged_total", writes_merged, labels) metric_integer("node_disk_written_bytes_total", (tonumber(written_sectors) or 0) * 512, labels) metric_fraction("node_disk_write_time_seconds_total", write_ms, 1000, labels) metric("node_disk_io_now", io_now, labels) metric_fraction("node_disk_io_time_seconds_total", io_ms, 1000, labels) metric_fraction("node_disk_io_time_weighted_seconds_total", io_weighted_ms, 1000, labels) if discard_ms ~= "" then metric("node_disk_discards_completed_total", discards, labels) metric("node_disk_discards_merged_total", discards_merged, labels) metric("node_disk_discarded_sectors_total", discarded_sectors, labels) metric_fraction("node_disk_discard_time_seconds_total", discard_ms, 1000, labels) end if flush_ms ~= "" then metric("node_disk_flush_requests_total", flushes, labels) metric_fraction("node_disk_flush_requests_time_seconds_total", flush_ms, 1000, labels) end end local function collect_diskstats() help("node_disk_reads_completed_total", "The total number of reads completed successfully.", "counter") help("node_disk_read_bytes_total", "The total number of bytes read successfully.", "counter") help("node_disk_writes_completed_total", "The total number of writes completed successfully.", "counter") help("node_disk_written_bytes_total", "The total number of bytes written successfully.", "counter") local f = io.open("/proc/diskstats", "r") if not f then return end for line in f:lines() do -- Cheap device-name match first; most lines (partitions, loop/ram devices) are excluded. local dev = match(line, "^%s*%d+%s+%d+%s+(%S+)") if dev and not disk_excluded(dev) then disk_metrics('{device="' .. label(dev) .. '"}', line) end end f:close() end local netdev_names = { "node_network_receive_bytes_total", "node_network_receive_packets_total", "node_network_receive_errs_total", "node_network_receive_drop_total", "node_network_receive_fifo_total", "node_network_receive_frame_total", "node_network_receive_compressed_total", "node_network_receive_multicast_total", "node_network_transmit_bytes_total", "node_network_transmit_packets_total", "node_network_transmit_errs_total", "node_network_transmit_drop_total", "node_network_transmit_fifo_total", "node_network_transmit_colls_total", "node_network_transmit_carrier_total", "node_network_transmit_compressed_total", } local netdev_pattern = "^%s*([^:]+):%s*(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)" .. "%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)" local function collect_netdev() local devices, seen = {}, {} help("node_network_receive_bytes_total", "Network device statistic receive_bytes.", "counter") help("node_network_transmit_bytes_total", "Network device statistic transmit_bytes.", "counter") local f = io.open("/proc/net/dev", "r") if not f then return devices end for line in f:lines() do local v = {match(line, netdev_pattern)} local iface = v[1] if iface then if not seen[iface] then devices[#devices + 1], seen[iface] = iface, true end local labels = '{device="' .. label(iface) .. '"}' for i = 1, 16 do write(netdev_names[i], labels, " ", v[i + 1], "\n") end end end f:close() return devices end local function collect_netclass(devices) help("node_network_info", "Non-numeric data from /sys/class/net.", "gauge") for i = 1, #devices do local iface = devices[i] local base = "/sys/class/net/" .. iface .. "/" local operstate = read_line(base .. "operstate") or "unknown" write(format('node_network_info{address="%s",broadcast="%s",device="%s",duplex="%s",ifalias="%s",operstate="%s"} 1\n', label(read_line(base .. "address") or ""), label(read_line(base .. "broadcast") or ""), label(iface), label(read_line(base .. "duplex") or ""), label(read_line(base .. "ifalias") or ""), label(operstate))) local labels = '{device="' .. label(iface) .. '"}' metric("node_network_up", operstate == "up" and 1 or 0, labels) metric("node_network_carrier", read_line(base .. "carrier"), labels) metric("node_network_carrier_changes_total", read_line(base .. "carrier_changes"), labels) metric("node_network_mtu_bytes", read_line(base .. "mtu"), labels) local speed = tonumber(read_line(base .. "speed")) if speed and speed >= 0 then metric_integer("node_network_speed_bytes", speed * 1000000 / 8, labels) end end end -- /proc/net/snmp and /proc/net/netstat: pairs of header and value lines. local function collect_netstat(path) local f = io.open(path, "r") if not f then return end while true do local header, values = f:read("*l"), f:read("*l") if not header or not values then break end local hi, vi = gmatch(header, "%S+"), gmatch(values, "%S+") local hp, vp = hi(), vi() if hp and hp == vp then if byte(hp, -1) == 58 then hp = sub(hp, 1, -2) end -- strip trailing ':' for key in hi do local value = vi() if not value then break end if find(value, "^%-?%d+$") then if find(key, "[^%w_]") then key = gsub(key, "[^%w_]", "_") end write("node_netstat_", hp, "_", key, " ", value, "\n") end end end end f:close() end local function collect_sockstat() local function one(path) local f = io.open(path, "r") if not f then return end for line in f:lines() do local proto, rest = match(line, "^([^:%s]+):%s*(.*)$") if proto then for key, value in gmatch(rest, "(%S+)%s+(%S+)") do if find(value, "^%d+$") then if find(key, "[^%w_]") then key = gsub(key, "[^%w_]", "_") end write("node_sockstat_", proto, "_", key, " ", value, "\n") end end end end f:close() end one("/proc/net/sockstat"); one("/proc/net/sockstat6") end local excluded_fstype = { autofs=true, binfmt_misc=true, bpf=true, cgroup=true, cgroup2=true, configfs=true, debugfs=true, devpts=true, devtmpfs=true, fusectl=true, hugetlbfs=true, mqueue=true, nsfs=true, proc=true, procfs=true, pstore=true, rpc_pipefs=true, securityfs=true, sysfs=true, tracefs=true } local mount_escapes = { ["\\040"] = " ", ["\\011"] = "\t", ["\\012"] = "\n", ["\\134"] = "\\" } local function decode_mount(value) if not find(value, "\\", 1, true) then return value end return (gsub(value, "\\%d%d%d", mount_escapes)) end local function mount_excluded(mp) return mp == "/dev" or find(mp, "^/dev/") or mp == "/proc" or find(mp, "^/proc/") or mp == "/sys" or find(mp, "^/sys/") or find(mp, "^/run/credentials/") or find(mp, "^/var/lib/docker/") or find(mp, "^/var/lib/containers/storage/") end -- statvfs binding, if one is installed (LuCI's nixio or luaposix); nil otherwise. local statvfs do local ok, mod = pcall(require, "nixio.fs") if ok and type(mod) == "table" and mod.statvfs then statvfs = mod.statvfs else ok, mod = pcall(require, "posix.sys.statvfs") if ok and type(mod) == "table" and mod.statvfs then statvfs = mod.statvfs end end end local function collect_filesystem() if getenv("NODE_EXPORTER_NO_DF") == "1" then return end local mounts, order = {}, {} each_line("/proc/self/mounts", function(line) local dev, mp, fstype, options = match(line, "^(%S+)%s+(%S+)%s+(%S+)%s+(%S+)") if not dev or excluded_fstype[fstype] then return end dev, mp = decode_mount(dev), decode_mount(mp) if mount_excluded(mp) then return end if not mounts[mp] then order[#order + 1] = mp end mounts[mp] = { labels = format('{device="%s",fstype="%s",mountpoint="%s"}', label(dev), label(fstype), label(mp)), readonly = find("," .. options .. ",", ",ro,", 1, true) and 1 or 0, } end) help("node_filesystem_avail_bytes", "Filesystem space available to non-root users in bytes.", "gauge") help("node_filesystem_free_bytes", "Filesystem free space in bytes.", "gauge") help("node_filesystem_size_bytes", "Filesystem size in bytes.", "gauge") if statvfs then for i = 1, #order do local mp = order[i] local m = mounts[mp] local ok, st = pcall(statvfs, mp) if ok and type(st) == "table" then -- nixio uses bare field names, luaposix prefixes them with f_. local bs = st.frsize or st.f_frsize or st.bsize or st.f_bsize local blocks, bfree, bavail = st.blocks or st.f_blocks, st.bfree or st.f_bfree, st.bavail or st.f_bavail if bs and blocks then m.files, m.ffree = st.files or st.f_files, st.ffree or st.f_ffree metric_integer("node_filesystem_avail_bytes", (bavail or 0) * bs, m.labels) metric_integer("node_filesystem_free_bytes", (bfree or 0) * bs, m.labels) metric_integer("node_filesystem_size_bytes", blocks * bs, m.labels) metric("node_filesystem_readonly", m.readonly, m.labels) metric("node_filesystem_device_error", 0, m.labels) else metric("node_filesystem_device_error", 1, m.labels) end else metric("node_filesystem_device_error", 1, m.labels) end end for i = 1, #order do local m = mounts[order[i]] if m.files then metric_integer("node_filesystem_files", m.files, m.labels) metric_integer("node_filesystem_files_free", m.ffree, m.labels) end end return end -- Fallback: one shell, two df runs. POSIX -k is also supported by older -- BusyBox df versions which lack GNU -B1. local df = "/bin/df" local probe = io.open("/usr/bin/df", "r") if probe then probe:close(); df = "/usr/bin/df" end local pipe = io.popen("LC_ALL=C " .. df .. " -P -k 2>&1; echo @@INODES; LC_ALL=C " .. df .. " -P -i 2>&1", "r") if not pipe then return end local inode, header, failed = false, true, false for line in pipe:lines() do if line == "@@INODES" then inode, header, failed = true, true, false elseif find(line, "^df: invalid option") then failed = true elseif header then header = false elseif not failed then local size, used, avail, mp = match(line, "^%S+%s+(%d+)%s+(%d+)%s+(%d+)%s+%S+%s+(.+)$") local m = mp and mounts[mp] if m then if inode then m.files, m.ffree = size, avail else m.size, m.used, m.avail = size, used, avail end end end end pipe:close() for i = 1, #order do local m = mounts[order[i]] if m.size then local size, used, avail = tonumber(m.size), tonumber(m.used), tonumber(m.avail) metric_integer("node_filesystem_avail_bytes", avail * 1024, m.labels) metric_integer("node_filesystem_free_bytes", (size - used) * 1024, m.labels) metric_integer("node_filesystem_size_bytes", size * 1024, m.labels) metric("node_filesystem_readonly", m.readonly, m.labels) metric("node_filesystem_device_error", 0, m.labels) end end for i = 1, #order do local m = mounts[order[i]] if m.files then metric("node_filesystem_files", m.files, m.labels) metric("node_filesystem_files_free", m.ffree, m.labels) end end end local function collect_filefd_entropy_conntrack() local allocated, maximum = match(read_line("/proc/sys/fs/file-nr") or "", "^(%d+)%s+%d+%s+(%d+)") if allocated then help("node_filefd_allocated", "File descriptor statistics: allocated.", "gauge") metric("node_filefd_allocated", allocated) help("node_filefd_maximum", "File descriptor statistics: maximum.", "gauge") metric("node_filefd_maximum", maximum) end local entropy = read_line("/proc/sys/kernel/random/entropy_avail") if entropy then help("node_entropy_available_bits", "Bits of available entropy.", "gauge") metric("node_entropy_available_bits", entropy) end metric("node_nf_conntrack_entries", read_line("/proc/sys/net/netfilter/nf_conntrack_count")) metric("node_nf_conntrack_entries_limit", read_line("/proc/sys/net/netfilter/nf_conntrack_max")) end local pressure_resources = {"cpu", "io", "memory", "irq"} local function collect_pressure() for i = 1, #pressure_resources do local resource = pressure_resources[i] each_line("/proc/pressure/" .. resource, function(line) local kind, total = match(line, "^(%S+).+total=(%d+)") if kind then local state = kind == "some" and "waiting" or (kind == "full" and "stalled" or nil) if state then metric_fraction("node_pressure_" .. resource .. "_" .. state .. "_seconds_total", total, 1000000) end end end) end end -- Probe 1_input, 2_input, ... Stops after 4 consecutive -- misses once a sensor was found, or after 8 misses if none was. local function indexed_inputs(base, stem, maximum, callback) local misses, found = 0, false for i = 1, maximum do local prefix = base .. stem .. i local value = read_line(prefix .. "_input") if value then found, misses = true, 0 callback(prefix, stem .. i, value) else misses = misses + 1 if misses >= (found and 4 or 8) then break end end end end local function collect_hwmon_thermal() local misses = 0 for h = 0, 63 do local base = "/sys/class/hwmon/hwmon" .. h .. "/" local chip = read_line(base .. "name") if chip then misses = 0 chip = label(chip) indexed_inputs(base, "temp", 32, function(prefix, sensor, value) sensor = read_line(prefix .. "_label") or sensor metric_fraction("node_hwmon_temp_celsius", value, 1000, '{chip="' .. chip .. '",sensor="' .. label(sensor) .. '"}') end) indexed_inputs(base, "fan", 32, function(prefix, sensor, value) sensor = read_line(prefix .. "_label") or sensor metric("node_hwmon_fan_rpm", value, '{chip="' .. chip .. '",sensor="' .. label(sensor) .. '"}') end) else misses = misses + 1 if misses >= 4 then break end end end misses = 0 for z = 0, 255 do local base = "/sys/class/thermal/thermal_zone" .. z .. "/" local value = read_line(base .. "temp") if value then misses = 0 metric_fraction("node_thermal_zone_temp", value, 1000, '{type="' .. label(read_line(base .. "type") or "unknown") .. '",zone="' .. z .. '"}') else misses = misses + 1 if misses >= 4 then break end end end end local cpufreq_files = { {"scaling_cur_freq", "node_cpu_scaling_frequency_hertz"}, {"cpuinfo_cur_freq", "node_cpu_frequency_hertz"}, {"cpuinfo_max_freq", "node_cpu_frequency_max_hertz"}, {"cpuinfo_min_freq", "node_cpu_frequency_min_hertz"}, } local function collect_cpufreq() for i = 1, #cpu_ids do local cpu = cpu_ids[i] local base = "/sys/devices/system/cpu/cpu" .. cpu .. "/cpufreq/" local labels = '{cpu="' .. cpu .. '"}' for j = 1, #cpufreq_files do local value = tonumber(read_line(base .. cpufreq_files[j][1])) if value then metric_integer(cpufreq_files[j][2], value * 1000, labels) end end end end local dmi_keys = { "bios_date", "bios_release", "bios_vendor", "bios_version", "board_asset_tag", "board_name", "board_vendor", "board_version", "chassis_asset_tag", "chassis_type", "chassis_vendor", "chassis_version", "product_family", "product_name", "product_sku", "product_uuid", "product_version", "system_vendor" } local function collect_dmi() if not read_line("/sys/class/dmi/id/product_name") then return end local labels = {} for i = 1, #dmi_keys do labels[i] = dmi_keys[i] .. '="' .. label(read_line("/sys/class/dmi/id/" .. dmi_keys[i])) .. '"' end help("node_dmi_info", "A metric with a constant 1 value labeled by DMI information.", "gauge") write("node_dmi_info{", concat(labels, ","), "} 1\n") end local function collect_self() local stat = read_line("/proc/self/stat") if stat then -- Fields after the command name: state ppid pgrp session tty tpgid flags minflt cminflt majflt cmajflt utime stime cutime cstime priority nice threads itrealvalue starttime local utime, stime, starttime = match(stat, "^%d+ %(.+%)%s+%S+%s+%S+%s+%S+%s+%S+%s+%S+%s+%S+%s+%S+%s+%S+%s+%S+%s+%S+%s+%S+%s+(%d+)%s+(%d+)%s+%S+%s+%S+%s+%S+%s+%S+%s+%S+%s+%S+%s+(%d+)") if utime then metric_fraction("process_cpu_seconds_total", (tonumber(utime) or 0) + (tonumber(stime) or 0), clk_tck) local status = read_all("/proc/self/status") or "" local rss = match(status, "VmRSS:%s+(%d+)") local size = match(status, "VmSize:%s+(%d+)") if rss then metric_integer("process_resident_memory_bytes", tonumber(rss) * 1024) end if size then metric_integer("process_virtual_memory_bytes", tonumber(size) * 1024) end local boot = boot_time if not boot then each_line("/proc/stat", function(line) if not boot then boot = match(line, "^btime%s+(%d+)") end end) end if boot then metric_fraction("process_start_time_seconds", tonumber(boot) * clk_tck + (tonumber(starttime) or 0), clk_tck) end end end local limits = read_all("/proc/self/limits") or "" metric("process_max_fds", match(limits, "Max open files%s+(%d+)")) end --------------------------------------------------------------------------- -- Main --------------------------------------------------------------------------- help("node_exporter_build_info", "A metric with a constant 1 value labeled by version information.", "gauge") write(format('node_exporter_build_info{branch="lua-cgi",goversion="",revision="",version="%s"} 1\n', label(_VERSION))) -- Each collector runs under pcall so one unexpected /proc format cannot take -- the whole scrape down; failures are reported via node_scrape_collector_success. local success = 1 local function run(collector, ...) local ok, result = pcall(collector, ...) if not ok then success = 0 io.stderr:write("node-exporter: ", tostring(result), "\n") return nil end return result end run(collect_uname_os) run(collect_stat) run(collect_loadavg) run(collect_meminfo) run(collect_vmstat) run(collect_diskstats) local network_devices = run(collect_netdev) or {} run(collect_netclass, network_devices) run(collect_netstat, "/proc/net/snmp") run(collect_netstat, "/proc/net/netstat") run(collect_sockstat) run(collect_filesystem) run(collect_filefd_entropy_conntrack) run(collect_pressure) run(collect_hwmon_thermal) run(collect_cpufreq) run(collect_dmi) run(collect_self) local scrape_finished = uptime() or os.clock() help("node_scrape_collector_success", "Whether the Lua collector succeeded.", "gauge") metric("node_scrape_collector_success", success, '{collector="lua"}') help("node_scrape_duration_seconds", "node-exporter-lua.cgi scrape duration.", "gauge") write(format("node_scrape_duration_seconds %.6f\n", scrape_finished - scrape_started)) flush()