Cache cpuidle names (was re-reading name sysfs every tick × cores) Cache CPU sysfs dir list + register cpufreq/throttle once Map for per-core prev lookups (was O(n²) .find) Dedupe meminfo/vmstat once per tick Cache iface speed/duplex/mtu/qlen (30s) Cache inotify limits (30s) Gate registerChart for IRQs, cpuidle, peardock mapped charts Remove leftover profile() stderr timers Cache os.cpus().length in docker collector Grow fd-cache buffer past 64KB for /proc/interrupts etc.
1340 lines
41 KiB
JavaScript
1340 lines
41 KiB
JavaScript
/**
|
|
* System metrics collector (~1s).
|
|
*
|
|
* Collects host-wide system / memory / disk / net / IP stats plus
|
|
* per-CPU, per-disk, per-iface, and per-mount instance charts.
|
|
* Linux uses /proc + /sys; other platforms fall back to Node `os`.
|
|
*/
|
|
import os from 'os'
|
|
import fs from 'fs'
|
|
import { EventEmitter } from 'events'
|
|
import {
|
|
SAMPLE_INTERVAL_MS,
|
|
getAllChartDefs,
|
|
registerChart,
|
|
makeCpuCoreChart,
|
|
makeDiskIoChart,
|
|
makeDiskOpsChart,
|
|
makeDiskUtilChart,
|
|
makeDiskAwaitChart,
|
|
makeDiskAvgszChart,
|
|
makeDiskQopsChart,
|
|
makeDiskBusyChart,
|
|
makeDiskIotimeChart,
|
|
makeDiskDiscardChart,
|
|
makeDiskSvctmChart,
|
|
makeDiskMergedChart,
|
|
makeDiskFlushChart,
|
|
makeNetChart,
|
|
makeNetPacketsChart,
|
|
makeNetErrorsChart,
|
|
makeNetDropsChart,
|
|
makeNetSpeedChart,
|
|
makeNetDuplexChart,
|
|
makeNetMtuChart,
|
|
makeNetQueueChart,
|
|
makeDiskSpaceChart,
|
|
makeDiskInodesChart,
|
|
} from '../../shared/metrics.js'
|
|
import {
|
|
collectCpuSysfs,
|
|
collectMemDeep,
|
|
collectNetDeep,
|
|
parsePressureFull,
|
|
ifaceSpeedKbps,
|
|
ifaceLinkMeta,
|
|
} from './collectors/host-deep.js'
|
|
import { collectHostMore } from './collectors/host-more.js'
|
|
import { readFileCached, closeFdCache } from '../utils/fd-cache.js'
|
|
import logger from '../utils/logger.js'
|
|
|
|
const log = logger.child('collector')
|
|
|
|
const IS_LINUX = os.platform() === 'linux'
|
|
|
|
function safeProcessUptime() {
|
|
try {
|
|
if (typeof process !== 'undefined' && typeof process.uptime === 'function') {
|
|
return process.uptime()
|
|
}
|
|
} catch {
|
|
// Bare / restricted hosts may deny process.uptime
|
|
}
|
|
return 0
|
|
}
|
|
|
|
function bytesToMiB(n) {
|
|
return n / (1024 * 1024)
|
|
}
|
|
function bytesToGiB(n) {
|
|
return n / (1024 * 1024 * 1024)
|
|
}
|
|
function readFile(path) {
|
|
return IS_LINUX ? readFileCached(path) : null
|
|
}
|
|
function rate(prev, cur, dtSec) {
|
|
if (dtSec <= 0 || cur < prev) return 0
|
|
return (cur - prev) / dtSec
|
|
}
|
|
function pct(part, total) {
|
|
return total > 0 ? (part / total) * 100 : 0
|
|
}
|
|
|
|
function parseMeminfo() {
|
|
const raw = readFile('/proc/meminfo')
|
|
if (!raw) return null
|
|
/** @type {Record<string, number>} */
|
|
const out = {}
|
|
for (const line of raw.split('\n')) {
|
|
// Include keys like Active(anon)
|
|
const m = line.match(/^([\w()]+):\s+(\d+)/)
|
|
if (m) out[m[1]] = Number(m[2]) * 1024
|
|
}
|
|
return out
|
|
}
|
|
|
|
function parseLoadavg() {
|
|
const raw = readFile('/proc/loadavg')
|
|
if (!raw) {
|
|
const l = os.loadavg()
|
|
return { load1: l[0], load5: l[1], load15: l[2], running: 0, total: 0 }
|
|
}
|
|
const parts = raw.trim().split(/\s+/)
|
|
const [running, total] = (parts[3] || '0/0').split('/').map(Number)
|
|
return {
|
|
load1: Number(parts[0]),
|
|
load5: Number(parts[1]),
|
|
load15: Number(parts[2]),
|
|
running: running || 0,
|
|
total: total || 0,
|
|
}
|
|
}
|
|
|
|
/** @returns {{ aggregate: object, cores: object[], intr: number, ctxt: number, processes: number, procsRunning: number, procsBlocked: number }|null} */
|
|
function parseProcStat() {
|
|
const raw = readFile('/proc/stat')
|
|
if (!raw) return null
|
|
const cores = []
|
|
let aggregate = null
|
|
let intr = 0
|
|
let ctxt = 0
|
|
let processes = 0
|
|
let procsRunning = 0
|
|
let procsBlocked = 0
|
|
|
|
for (const line of raw.split('\n')) {
|
|
const p = line.trim().split(/\s+/)
|
|
if (!p[0]) continue
|
|
if (p[0] === 'cpu') {
|
|
// user nice system idle iowait irq softirq steal guest guest_nice
|
|
aggregate = {
|
|
user: Number(p[1]) || 0,
|
|
nice: Number(p[2]) || 0,
|
|
system: Number(p[3]) || 0,
|
|
idle: Number(p[4]) || 0,
|
|
iowait: Number(p[5]) || 0,
|
|
irq: Number(p[6]) || 0,
|
|
softirq: Number(p[7]) || 0,
|
|
steal: Number(p[8]) || 0,
|
|
guest: Number(p[9]) || 0,
|
|
guest_nice: Number(p[10]) || 0,
|
|
}
|
|
} else if (/^cpu\d+$/.test(p[0])) {
|
|
cores.push({
|
|
id: p[0].slice(3),
|
|
user: Number(p[1]) || 0,
|
|
nice: Number(p[2]) || 0,
|
|
system: Number(p[3]) || 0,
|
|
idle: Number(p[4]) || 0,
|
|
iowait: Number(p[5]) || 0,
|
|
irq: Number(p[6]) || 0,
|
|
softirq: Number(p[7]) || 0,
|
|
steal: Number(p[8]) || 0,
|
|
guest: Number(p[9]) || 0,
|
|
guest_nice: Number(p[10]) || 0,
|
|
})
|
|
} else if (p[0] === 'intr') intr = Number(p[1]) || 0
|
|
else if (p[0] === 'ctxt') ctxt = Number(p[1]) || 0
|
|
else if (p[0] === 'processes') processes = Number(p[1]) || 0
|
|
else if (p[0] === 'procs_running') procsRunning = Number(p[1]) || 0
|
|
else if (p[0] === 'procs_blocked') procsBlocked = Number(p[1]) || 0
|
|
}
|
|
return { aggregate, cores, intr, ctxt, processes, procsRunning, procsBlocked }
|
|
}
|
|
|
|
/**
|
|
* Convert /proc/stat jiffy counters to percentages.
|
|
* Linux accounts guest in user and guest_nice in nice — subtract before totaling
|
|
* (same approach as htop/netdata) so idle isn't understated on hypervisor hosts.
|
|
*/
|
|
function cpuDeltaPct(prev, cur) {
|
|
if (!prev || !cur) {
|
|
return {
|
|
guest_nice: 0,
|
|
guest: 0,
|
|
steal: 0,
|
|
softirq: 0,
|
|
irq: 0,
|
|
user: 0,
|
|
system: 0,
|
|
nice: 0,
|
|
iowait: 0,
|
|
idle: 100,
|
|
}
|
|
}
|
|
const delta = (k) => Math.max(0, (cur[k] || 0) - (prev[k] || 0))
|
|
const guest = delta('guest')
|
|
const guestNice = delta('guest_nice')
|
|
// guest ⊆ user, guest_nice ⊆ nice in kernel counters
|
|
const user = Math.max(0, delta('user') - guest)
|
|
const nice = Math.max(0, delta('nice') - guestNice)
|
|
const d = {
|
|
guest_nice: guestNice,
|
|
guest,
|
|
steal: delta('steal'),
|
|
softirq: delta('softirq'),
|
|
irq: delta('irq'),
|
|
user,
|
|
system: delta('system'),
|
|
nice,
|
|
iowait: delta('iowait'),
|
|
idle: delta('idle'),
|
|
}
|
|
let total = 0
|
|
for (const v of Object.values(d)) total += v
|
|
if (total <= 0) total = 1
|
|
return {
|
|
guest_nice: pct(d.guest_nice, total),
|
|
guest: pct(d.guest, total),
|
|
steal: pct(d.steal, total),
|
|
softirq: pct(d.softirq, total),
|
|
irq: pct(d.irq, total),
|
|
user: pct(d.user, total),
|
|
system: pct(d.system, total),
|
|
nice: pct(d.nice, total),
|
|
iowait: pct(d.iowait, total),
|
|
idle: pct(d.idle, total),
|
|
}
|
|
}
|
|
|
|
function parseNetDev() {
|
|
const raw = readFile('/proc/net/dev')
|
|
/** @type {Record<string, { rxBytes: number, txBytes: number, rxPackets: number, txPackets: number, rxErrs: number, txErrs: number, rxDrop: number, txDrop: number, rxMulti: number }>} */
|
|
const ifaces = {}
|
|
if (!raw) return ifaces
|
|
for (const line of raw.split('\n').slice(2)) {
|
|
const parts = line.trim().split(/\s+/)
|
|
if (parts.length < 17) continue
|
|
const name = parts[0].replace(':', '')
|
|
if (!name || name === 'lo') continue
|
|
ifaces[name] = {
|
|
rxBytes: Number(parts[1]) || 0,
|
|
rxPackets: Number(parts[2]) || 0,
|
|
rxErrs: Number(parts[3]) || 0,
|
|
rxDrop: Number(parts[4]) || 0,
|
|
rxMulti: Number(parts[8]) || 0,
|
|
txBytes: Number(parts[9]) || 0,
|
|
txPackets: Number(parts[10]) || 0,
|
|
txErrs: Number(parts[11]) || 0,
|
|
txDrop: Number(parts[12]) || 0,
|
|
}
|
|
}
|
|
return ifaces
|
|
}
|
|
|
|
function parseDiskstats() {
|
|
const raw = readFile('/proc/diskstats')
|
|
/** @type {Record<string, {
|
|
* reads: number, readSectors: number, readMs: number,
|
|
* writes: number, writeSectors: number, writeMs: number,
|
|
* ioMs: number, ioInFlight: number,
|
|
* discards: number, discardSectors: number
|
|
* }>} */
|
|
const disks = {}
|
|
if (!raw) return disks
|
|
for (const line of raw.split('\n')) {
|
|
const p = line.trim().split(/\s+/)
|
|
if (p.length < 14) continue
|
|
const name = p[2]
|
|
if (/^(loop|ram|fd|sr)/.test(name)) continue
|
|
const includeParts =
|
|
process.env.PEARDATA_DISK_PARTITIONS === '1' ||
|
|
process.env.PEARDATA_DISK_PARTITIONS === 'on'
|
|
if (!includeParts) {
|
|
if (/^nvme\d+n\d+p\d+$/.test(name)) continue
|
|
if (/^(sd|vd|xvd|hd)[a-z]+\d+$/.test(name)) continue
|
|
if (/^mmcblk\d+p\d+$/.test(name)) continue
|
|
}
|
|
disks[name] = {
|
|
reads: Number(p[3]) || 0,
|
|
readMerged: Number(p[4]) || 0,
|
|
readSectors: Number(p[5]) || 0,
|
|
readMs: Number(p[6]) || 0,
|
|
writes: Number(p[7]) || 0,
|
|
writeMerged: Number(p[8]) || 0,
|
|
writeSectors: Number(p[9]) || 0,
|
|
writeMs: Number(p[10]) || 0,
|
|
ioInFlight: Number(p[11]) || 0,
|
|
ioMs: Number(p[12]) || 0,
|
|
discards: p.length > 14 ? Number(p[14]) || 0 : 0,
|
|
discardSectors: p.length > 16 ? Number(p[16]) || 0 : 0,
|
|
flushes: p.length > 18 ? Number(p[18]) || 0 : 0,
|
|
}
|
|
}
|
|
return disks
|
|
}
|
|
|
|
function parseVmstat() {
|
|
const raw = readFile('/proc/vmstat')
|
|
if (!raw) return null
|
|
/** @type {Record<string, number>} */
|
|
const out = {}
|
|
for (const line of raw.split('\n')) {
|
|
const [k, v] = line.trim().split(/\s+/)
|
|
if (k && v != null) out[k] = Number(v) || 0
|
|
}
|
|
return out
|
|
}
|
|
|
|
function parseSnmp() {
|
|
const raw = readFile('/proc/net/snmp')
|
|
if (!raw) return { ip: {}, tcp: {}, udp: {}, icmp: {} }
|
|
/** @type {Record<string, Record<string, number>>} */
|
|
const tables = {}
|
|
const lines = raw.split('\n')
|
|
for (let i = 0; i + 1 < lines.length; i += 2) {
|
|
const header = lines[i].trim().split(/\s+/)
|
|
const values = lines[i + 1].trim().split(/\s+/)
|
|
const name = header[0].replace(':', '')
|
|
/** @type {Record<string, number>} */
|
|
const row = {}
|
|
for (let j = 1; j < header.length; j++) {
|
|
row[header[j]] = Number(values[j]) || 0
|
|
}
|
|
tables[name.toLowerCase()] = row
|
|
}
|
|
return {
|
|
ip: tables.ip || {},
|
|
tcp: tables.tcp || {},
|
|
udp: tables.udp || {},
|
|
icmp: tables.icmp || {},
|
|
}
|
|
}
|
|
|
|
function parseSockstat() {
|
|
const raw = readFile('/proc/net/sockstat')
|
|
if (!raw) return { tcpInuse: 0 }
|
|
const m = raw.match(/TCP:\s+inuse\s+(\d+)/)
|
|
return { tcpInuse: m ? Number(m[1]) : 0 }
|
|
}
|
|
|
|
function parsePressure(kind) {
|
|
return parsePressureFull(kind)
|
|
}
|
|
|
|
function parseMounts() {
|
|
const raw = readFile('/proc/mounts')
|
|
if (!raw) return []
|
|
const skipFs = new Set([
|
|
'proc',
|
|
'sysfs',
|
|
'devtmpfs',
|
|
'devpts',
|
|
'tmpfs',
|
|
'cgroup',
|
|
'cgroup2',
|
|
'pstore',
|
|
'bpf',
|
|
'debugfs',
|
|
'tracefs',
|
|
'securityfs',
|
|
'fusectl',
|
|
'configfs',
|
|
'mqueue',
|
|
'hugetlbfs',
|
|
'rpc_pipefs',
|
|
'binfmt_misc',
|
|
'autofs',
|
|
'overlay',
|
|
'nsfs',
|
|
])
|
|
/** @type {Array<{ path: string, fs: string }>} */
|
|
const mounts = []
|
|
for (const line of raw.split('\n')) {
|
|
const p = line.split(/\s+/)
|
|
if (p.length < 3) continue
|
|
const path = p[1]
|
|
const fsType = p[2]
|
|
if (skipFs.has(fsType)) continue
|
|
if (!path.startsWith('/')) continue
|
|
// limit to root + first-level mounts to keep overhead low
|
|
if (path !== '/' && path.split('/').filter(Boolean).length > 2) continue
|
|
mounts.push({ path, fs: fsType })
|
|
}
|
|
return mounts
|
|
}
|
|
|
|
function mountId(path) {
|
|
if (path === '/') return 'root'
|
|
return path.replace(/^\//, '').replace(/[^\w.-]+/g, '_')
|
|
}
|
|
|
|
function sampleMount(path) {
|
|
try {
|
|
const st = fs.statfsSync(path)
|
|
const bsize = Number(st.bsize) || 4096
|
|
const blocks = Number(st.blocks) || 0
|
|
const bfree = Number(st.bfree) || 0
|
|
const bavail = Number(st.bavail) || 0
|
|
const files = Number(st.files) || 0
|
|
const ffree = Number(st.ffree) || 0
|
|
const total = blocks * bsize
|
|
const free = bfree * bsize
|
|
const avail = bavail * bsize
|
|
const used = Math.max(0, total - free)
|
|
const reserved = Math.max(0, free - avail)
|
|
const inodesUsed = Math.max(0, files - ffree)
|
|
return {
|
|
space: {
|
|
avail: bytesToGiB(avail),
|
|
used: bytesToGiB(used),
|
|
reserved_for_root: bytesToGiB(reserved),
|
|
},
|
|
inodes: {
|
|
avail: ffree,
|
|
used: inodesUsed,
|
|
reserved_for_root: 0,
|
|
},
|
|
}
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
export class MetricsCollector extends EventEmitter {
|
|
constructor(opts = {}) {
|
|
super()
|
|
this.intervalMs =
|
|
opts.intervalMs ?? (Number(process.env.PEARDATA_SAMPLE_MS) || SAMPLE_INTERVAL_MS)
|
|
this.timer = null
|
|
this.running = false
|
|
this.sampleCount = 0
|
|
this.lastTs = 0
|
|
this.lastStat = null
|
|
this.lastNet = null
|
|
this.lastDisk = null
|
|
this.lastVm = null
|
|
this.lastSnmp = null
|
|
/** Deep collector state (freq/throttle/softnet/TcpExt) */
|
|
this.deepState = {}
|
|
/** @type {Map<string, Record<string, number|null>>} */
|
|
this.latest = new Map()
|
|
this._chartsRegistered = false
|
|
this._lastCpuUsage = process.cpuUsage()
|
|
this._lastCpuTs = Date.now()
|
|
}
|
|
|
|
start() {
|
|
if (this.running) return
|
|
this.running = true
|
|
this._tick()
|
|
this.timer = setInterval(() => this._tick(), this.intervalMs)
|
|
if (this.timer.unref) this.timer.unref()
|
|
log.info('Collector started', { intervalMs: this.intervalMs })
|
|
}
|
|
|
|
stop() {
|
|
this.running = false
|
|
if (this.timer) clearInterval(this.timer)
|
|
this.timer = null
|
|
if (IS_LINUX) closeFdCache()
|
|
}
|
|
|
|
_tick() {
|
|
const t0 = performance.now()
|
|
try {
|
|
const ts = Date.now()
|
|
const dtSec = this.lastTs ? (ts - this.lastTs) / 1000 : this.intervalMs / 1000
|
|
this.lastTs = ts
|
|
const prevVmSnapshot = this.lastVm
|
|
|
|
/** @type {Array<{ chart: string, context: string, ts: number, values: Record<string, number|null> }>} */
|
|
const batch = []
|
|
|
|
if (IS_LINUX) {
|
|
const memSnap = parseMeminfo()
|
|
const vmSnap = parseVmstat()
|
|
this._safeCollect('cpu', () => this._collectCpuAndProcs(batch, ts, dtSec))
|
|
this._safeCollect('cpu_sysfs', () => collectCpuSysfs(batch, ts, dtSec, this.deepState))
|
|
this._safeCollect('memory', () => this._collectMemory(batch, ts, dtSec, memSnap, vmSnap))
|
|
this._safeCollect('disk', () => this._collectDisk(batch, ts, dtSec))
|
|
this._safeCollect('net', () => this._collectNet(batch, ts, dtSec))
|
|
this._safeCollect('ip', () => this._collectIp(batch, ts, dtSec))
|
|
this._safeCollect('pressure', () => this._collectPressure(batch, ts))
|
|
this._safeCollect('diskspace', () => this._collectDiskSpace(batch, ts))
|
|
this._safeCollect('host_more', () => {
|
|
collectHostMore(batch, ts, dtSec, this.deepState, {
|
|
mem: memSnap,
|
|
vm: vmSnap,
|
|
prevVm: prevVmSnapshot,
|
|
})
|
|
})
|
|
} else {
|
|
this._safeCollect('cpu', () => this._collectCpuFallback(batch, ts, dtSec))
|
|
this._safeCollect('memory', () => this._collectMemoryFallback(batch, ts))
|
|
}
|
|
|
|
let uptime = 0
|
|
try {
|
|
uptime = os.uptime()
|
|
} catch {
|
|
if (IS_LINUX) {
|
|
const raw = readFile('/proc/uptime')
|
|
if (raw) uptime = Number(raw.trim().split(/\s+/)[0]) || 0
|
|
}
|
|
}
|
|
batch.push({
|
|
chart: 'system.uptime',
|
|
context: 'system.uptime',
|
|
ts,
|
|
values: { uptime },
|
|
})
|
|
|
|
if (IS_LINUX) {
|
|
const entropy = readFile('/proc/sys/kernel/random/entropy_avail')
|
|
if (entropy != null) {
|
|
batch.push({
|
|
chart: 'system.entropy',
|
|
context: 'system.entropy',
|
|
ts,
|
|
values: { entropy: Number(entropy.trim()) || 0 },
|
|
})
|
|
}
|
|
}
|
|
|
|
for (const s of batch) this.latest.set(s.chart, s.values)
|
|
this.sampleCount++
|
|
this._chartsRegistered = true
|
|
this.emit('samples', batch)
|
|
} catch (err) {
|
|
log.error('Collector tick failed', { error: err.message })
|
|
}
|
|
const dur = performance.now() - t0
|
|
if (dur > 15) {
|
|
const now = Date.now()
|
|
const cpuDelta = process.cpuUsage(this._lastCpuUsage)
|
|
const wallMs = now - this._lastCpuTs
|
|
this._lastCpuUsage = process.cpuUsage()
|
|
this._lastCpuTs = now
|
|
const totalMs = (cpuDelta.user + cpuDelta.system) / 1000
|
|
const pct = wallMs > 0 ? Math.round((totalMs / wallMs) * 100) : 0
|
|
if (pct > 30) {
|
|
log.debug('tick', { collector: 'main', durMs: Math.round(dur * 10) / 10, cpuPct: pct, cpuUserMs: Math.round(cpuDelta.user / 1000), cpuSysMs: Math.round(cpuDelta.system / 1000), wallMs })
|
|
} else {
|
|
log.debug('tick', { collector: 'main', durMs: Math.round(dur * 10) / 10 })
|
|
}
|
|
}
|
|
}
|
|
|
|
_safeCollect(name, fn) {
|
|
const t0 = performance.now()
|
|
try {
|
|
fn()
|
|
} catch (err) {
|
|
log.warn(`Collector ${name} failed`, { error: err.message })
|
|
}
|
|
const dur = performance.now() - t0
|
|
if (dur > 5) log.debug(`collect:${name}`, { durMs: Math.round(dur * 10) / 10 })
|
|
}
|
|
|
|
_collectCpuFallback(batch, ts, dtSec) {
|
|
const cpus = os.cpus()
|
|
const load = os.loadavg()
|
|
const agg = { user: 0, nice: 0, system: 0, idle: 0, irq: 0, iowait: 0, softirq: 0, steal: 0, guest: 0, guest_nice: 0 }
|
|
const prevById = this._lastStatCoresById || new Map()
|
|
/** @type {Map<string, object>} */
|
|
const nextById = new Map()
|
|
for (let i = 0; i < cpus.length; i++) {
|
|
const c = cpus[i]
|
|
const cur = { id: String(i), user: c.times.user, nice: c.times.nice, system: c.times.sys, idle: c.times.idle, irq: c.times.irq, iowait: 0, softirq: 0, steal: 0, guest: 0, guest_nice: 0 }
|
|
if (!this._chartsRegistered) registerChart(makeCpuCoreChart(i))
|
|
const prev = prevById.get(String(i))
|
|
batch.push({ chart: `cpu.cpu${i}`, context: 'cpu.cpu', ts, values: cpuDeltaPct(prev, cur) })
|
|
for (const k of Object.keys(agg)) agg[k] += cur[k] || 0
|
|
nextById.set(String(i), cur)
|
|
}
|
|
batch.push({ chart: 'system.cpu', context: 'system.cpu', ts, values: cpuDeltaPct(this.lastStat?.aggregate, agg) })
|
|
this.lastStat = { aggregate: agg, cores: [...nextById.values()], intr: 0, ctxt: 0, processes: 0, procsRunning: 0, procsBlocked: 0 }
|
|
this._lastStatCoresById = nextById
|
|
batch.push({ chart: 'system.processes', context: 'system.processes', ts, values: { running: load[0] < 0 ? 0 : Math.round(load[0]), blocked: 0 } })
|
|
batch.push({ chart: 'system.active_processes', context: 'system.active_processes', ts, values: { active: 0 } })
|
|
batch.push({ chart: 'system.load', context: 'system.load', ts, values: { load1: load[0], load5: load[1], load15: load[2] } })
|
|
}
|
|
|
|
_collectMemoryFallback(batch, ts) {
|
|
const total = os.totalmem()
|
|
const free = os.freemem()
|
|
batch.push({ chart: 'system.ram', context: 'system.ram', ts, values: { free: bytesToMiB(free), used: bytesToMiB(total - free), cached: 0, buffers: 0 } })
|
|
batch.push({ chart: 'mem.available', context: 'mem.available', ts, values: { avail: bytesToMiB(free) } })
|
|
}
|
|
|
|
_collectCpuAndProcs(batch, ts, dtSec) {
|
|
const stat = parseProcStat()
|
|
const load = parseLoadavg()
|
|
|
|
if (stat?.aggregate) {
|
|
const cpuVals = cpuDeltaPct(this.lastStat?.aggregate, stat.aggregate)
|
|
batch.push({ chart: 'system.cpu', context: 'system.cpu', ts, values: cpuVals })
|
|
|
|
const prevById = this._lastStatCoresById || new Map()
|
|
/** @type {Map<string, object>} */
|
|
const nextById = new Map()
|
|
for (const core of stat.cores) {
|
|
if (!this._chartsRegistered) registerChart(makeCpuCoreChart(core.id))
|
|
const prev = prevById.get(core.id)
|
|
batch.push({
|
|
chart: `cpu.cpu${core.id}`,
|
|
context: 'cpu.cpu',
|
|
ts,
|
|
values: cpuDeltaPct(prev, core),
|
|
})
|
|
nextById.set(core.id, core)
|
|
}
|
|
|
|
const prev = this.lastStat
|
|
batch.push({
|
|
chart: 'system.intr',
|
|
context: 'system.intr',
|
|
ts,
|
|
values: { interrupts: prev ? rate(prev.intr, stat.intr, dtSec) : 0 },
|
|
})
|
|
batch.push({
|
|
chart: 'system.ctxt',
|
|
context: 'system.ctxt',
|
|
ts,
|
|
values: { switches: prev ? rate(prev.ctxt, stat.ctxt, dtSec) : 0 },
|
|
})
|
|
batch.push({
|
|
chart: 'system.forks',
|
|
context: 'system.forks',
|
|
ts,
|
|
values: { started: prev ? rate(prev.processes, stat.processes, dtSec) : 0 },
|
|
})
|
|
batch.push({
|
|
chart: 'system.processes',
|
|
context: 'system.processes',
|
|
ts,
|
|
values: { running: stat.procsRunning, blocked: stat.procsBlocked },
|
|
})
|
|
batch.push({
|
|
chart: 'system.active_processes',
|
|
context: 'system.active_processes',
|
|
ts,
|
|
values: { active: load.total || stat.procsRunning + stat.procsBlocked },
|
|
})
|
|
this.lastStat = stat
|
|
this._lastStatCoresById = nextById
|
|
} else {
|
|
// Node os fallback
|
|
const cpus = os.cpus()
|
|
const agg = { user: 0, nice: 0, system: 0, idle: 0, irq: 0, iowait: 0, softirq: 0, steal: 0, guest: 0, guest_nice: 0 }
|
|
const prevById = this._lastStatCoresById || new Map()
|
|
/** @type {Map<string, object>} */
|
|
const nextById = new Map()
|
|
cpus.forEach((c, i) => {
|
|
const cur = {
|
|
id: String(i),
|
|
user: c.times.user,
|
|
nice: c.times.nice,
|
|
system: c.times.sys,
|
|
idle: c.times.idle,
|
|
irq: c.times.irq,
|
|
iowait: 0,
|
|
softirq: 0,
|
|
steal: 0,
|
|
guest: 0,
|
|
guest_nice: 0,
|
|
}
|
|
if (!this._chartsRegistered) registerChart(makeCpuCoreChart(i))
|
|
const prevCore = prevById.get(String(i))
|
|
batch.push({
|
|
chart: `cpu.cpu${i}`,
|
|
context: 'cpu.cpu',
|
|
ts,
|
|
values: cpuDeltaPct(prevCore, cur),
|
|
})
|
|
for (const k of Object.keys(agg)) agg[k] += cur[k] || 0
|
|
nextById.set(String(i), cur)
|
|
})
|
|
batch.push({
|
|
chart: 'system.cpu',
|
|
context: 'system.cpu',
|
|
ts,
|
|
values: cpuDeltaPct(this.lastStat?.aggregate, agg),
|
|
})
|
|
this.lastStat = { aggregate: agg, cores: [...nextById.values()], intr: 0, ctxt: 0, processes: 0, procsRunning: 0, procsBlocked: 0 }
|
|
this._lastStatCoresById = nextById
|
|
batch.push({
|
|
chart: 'system.processes',
|
|
context: 'system.processes',
|
|
ts,
|
|
values: { running: load.running, blocked: 0 },
|
|
})
|
|
batch.push({
|
|
chart: 'system.active_processes',
|
|
context: 'system.active_processes',
|
|
ts,
|
|
values: { active: load.total || 0 },
|
|
})
|
|
}
|
|
|
|
batch.push({
|
|
chart: 'system.load',
|
|
context: 'system.load',
|
|
ts,
|
|
values: { load1: load.load1, load5: load.load5, load15: load.load15 },
|
|
})
|
|
}
|
|
|
|
_collectMemory(batch, ts, dtSec, memArg, vmArg) {
|
|
const mem = memArg || parseMeminfo()
|
|
const vm = vmArg || parseVmstat()
|
|
const total = os.totalmem()
|
|
const freeOs = os.freemem()
|
|
|
|
if (mem) {
|
|
const freeB = mem.MemFree || freeOs
|
|
const buffers = mem.Buffers || 0
|
|
// Shmem (tmpfs etc.) is already counted in Cached — subtract so used isn't understated
|
|
const cached = Math.max(
|
|
0,
|
|
(mem.Cached || 0) + (mem.SReclaimable || 0) - (mem.Shmem || 0)
|
|
)
|
|
const used = Math.max(0, (mem.MemTotal || total) - freeB - buffers - cached)
|
|
batch.push({
|
|
chart: 'system.ram',
|
|
context: 'system.ram',
|
|
ts,
|
|
values: {
|
|
free: bytesToMiB(freeB),
|
|
used: bytesToMiB(used),
|
|
cached: bytesToMiB(cached),
|
|
buffers: bytesToMiB(buffers),
|
|
},
|
|
})
|
|
batch.push({
|
|
chart: 'mem.available',
|
|
context: 'mem.available',
|
|
ts,
|
|
values: { avail: bytesToMiB(mem.MemAvailable || freeB) },
|
|
})
|
|
const swapTotal = mem.SwapTotal || 0
|
|
const swapFree = mem.SwapFree || 0
|
|
batch.push({
|
|
chart: 'mem.swap',
|
|
context: 'mem.swap',
|
|
ts,
|
|
values: {
|
|
free: bytesToMiB(swapFree),
|
|
used: bytesToMiB(Math.max(0, swapTotal - swapFree)),
|
|
},
|
|
})
|
|
batch.push({
|
|
chart: 'mem.swap_cached',
|
|
context: 'mem.swap_cached',
|
|
ts,
|
|
values: { cached: bytesToMiB(mem.SwapCached || 0) },
|
|
})
|
|
batch.push({
|
|
chart: 'mem.kernel',
|
|
context: 'mem.kernel',
|
|
ts,
|
|
values: {
|
|
slab: bytesToMiB(mem.Slab || 0),
|
|
kernel_stack: bytesToMiB(mem.KernelStack || 0),
|
|
page_tables: bytesToMiB(mem.PageTables || 0),
|
|
vmalloc_used: bytesToMiB(mem.VmallocUsed || 0),
|
|
percpu: bytesToMiB(mem.Percpu || 0),
|
|
},
|
|
})
|
|
const reclaimable = mem.SReclaimable || 0
|
|
const unreclaim = mem.SUnreclaim || Math.max(0, (mem.Slab || 0) - reclaimable)
|
|
batch.push({
|
|
chart: 'mem.slab',
|
|
context: 'mem.slab',
|
|
ts,
|
|
values: {
|
|
reclaimable: bytesToMiB(reclaimable),
|
|
unreclaimable: bytesToMiB(unreclaim),
|
|
},
|
|
})
|
|
batch.push({
|
|
chart: 'mem.writeback',
|
|
context: 'mem.writeback',
|
|
ts,
|
|
values: {
|
|
dirty: bytesToMiB(mem.Dirty || 0),
|
|
writeback: bytesToMiB(mem.Writeback || 0),
|
|
FuseWriteback: bytesToMiB(mem.WritebackTmp || 0),
|
|
NfsWriteback: bytesToMiB(mem.NFS_Unstable || 0),
|
|
Bounce: bytesToMiB(mem.Bounce || 0),
|
|
},
|
|
})
|
|
batch.push({
|
|
chart: 'mem.committed',
|
|
context: 'mem.committed',
|
|
ts,
|
|
values: { Committed_AS: bytesToMiB(mem.Committed_AS || 0) },
|
|
})
|
|
} else {
|
|
batch.push({
|
|
chart: 'system.ram',
|
|
context: 'system.ram',
|
|
ts,
|
|
values: {
|
|
free: bytesToMiB(freeOs),
|
|
used: bytesToMiB(total - freeOs),
|
|
cached: 0,
|
|
buffers: 0,
|
|
},
|
|
})
|
|
batch.push({
|
|
chart: 'mem.available',
|
|
context: 'mem.available',
|
|
ts,
|
|
values: { avail: bytesToMiB(freeOs) },
|
|
})
|
|
}
|
|
|
|
const prevVm = this.lastVm
|
|
if (vm) {
|
|
const page = 4
|
|
batch.push({
|
|
chart: 'mem.swapio',
|
|
context: 'mem.swapio',
|
|
ts,
|
|
values: {
|
|
in: prevVm ? rate(prevVm.pswpin || 0, vm.pswpin || 0, dtSec) * page : 0,
|
|
out: prevVm ? rate(prevVm.pswpout || 0, vm.pswpout || 0, dtSec) * page : 0,
|
|
},
|
|
})
|
|
batch.push({
|
|
chart: 'system.pgpgio',
|
|
context: 'system.pgpgio',
|
|
ts,
|
|
values: {
|
|
in: prevVm ? rate(prevVm.pgpgin || 0, vm.pgpgin || 0, dtSec) : 0,
|
|
out: prevVm ? rate(prevVm.pgpgout || 0, vm.pgpgout || 0, dtSec) : 0,
|
|
},
|
|
})
|
|
const minor = (vm.pgfault || 0) - (vm.pgmajfault || 0)
|
|
const prevMinor = prevVm ? (prevVm.pgfault || 0) - (prevVm.pgmajfault || 0) : 0
|
|
batch.push({
|
|
chart: 'system.pgfaults',
|
|
context: 'system.pgfaults',
|
|
ts,
|
|
values: {
|
|
minor: prevVm ? rate(prevMinor, minor, dtSec) : 0,
|
|
major: prevVm ? rate(prevVm.pgmajfault || 0, vm.pgmajfault || 0, dtSec) : 0,
|
|
},
|
|
})
|
|
}
|
|
if (mem) collectMemDeep(batch, ts, dtSec, mem, vm, prevVm)
|
|
if (vm) this.lastVm = vm
|
|
}
|
|
|
|
_collectDisk(batch, ts, dtSec) {
|
|
const disks = parseDiskstats()
|
|
let aggRead = 0
|
|
let aggWrite = 0
|
|
const prevAll = this.lastDisk || {}
|
|
|
|
for (const [name, d] of Object.entries(disks)) {
|
|
if (name === '__agg') continue
|
|
const registeredDisks = this._registeredDisks || (this._registeredDisks = new Set())
|
|
if (!registeredDisks.has(name)) {
|
|
registerChart(makeDiskIoChart(name))
|
|
registerChart(makeDiskOpsChart(name))
|
|
registerChart(makeDiskUtilChart(name))
|
|
registerChart(makeDiskAwaitChart(name))
|
|
registerChart(makeDiskAvgszChart(name))
|
|
registerChart(makeDiskQopsChart(name))
|
|
registerChart(makeDiskBusyChart(name))
|
|
registerChart(makeDiskIotimeChart(name))
|
|
if (d.discards != null) registerChart(makeDiskDiscardChart(name))
|
|
registerChart(makeDiskSvctmChart(name))
|
|
registerChart(makeDiskMergedChart(name))
|
|
registerChart(makeDiskFlushChart(name))
|
|
registeredDisks.add(name)
|
|
}
|
|
const prev = prevAll[name]
|
|
const readBytes = d.readSectors * 512
|
|
const writeBytes = d.writeSectors * 512
|
|
aggRead += readBytes
|
|
aggWrite += writeBytes
|
|
const readKiB = prev ? rate(prev.readBytes, readBytes, dtSec) / 1024 : 0
|
|
const writeKiB = prev ? rate(prev.writeBytes, writeBytes, dtSec) / 1024 : 0
|
|
const dReads = prev ? Math.max(0, d.reads - prev.reads) : 0
|
|
const dWrites = prev ? Math.max(0, d.writes - prev.writes) : 0
|
|
const dReadMs = prev ? Math.max(0, d.readMs - prev.readMs) : 0
|
|
const dWriteMs = prev ? Math.max(0, d.writeMs - prev.writeMs) : 0
|
|
const dReadSec = prev ? Math.max(0, d.readSectors - prev.readSectors) : 0
|
|
const dWriteSec = prev ? Math.max(0, d.writeSectors - prev.writeSectors) : 0
|
|
batch.push({
|
|
chart: `disk_io.${name}`,
|
|
context: 'disk.io',
|
|
ts,
|
|
values: { reads: readKiB, writes: writeKiB },
|
|
})
|
|
batch.push({
|
|
chart: `disk_ops.${name}`,
|
|
context: 'disk.ops',
|
|
ts,
|
|
values: {
|
|
reads: prev ? rate(prev.reads, d.reads, dtSec) : 0,
|
|
writes: prev ? rate(prev.writes, d.writes, dtSec) : 0,
|
|
},
|
|
})
|
|
const util = prev ? Math.min(100, rate(prev.ioMs, d.ioMs, dtSec) / 10) : 0
|
|
batch.push({
|
|
chart: `disk_util.${name}`,
|
|
context: 'disk.util',
|
|
ts,
|
|
values: { utilization: util },
|
|
})
|
|
batch.push({
|
|
chart: `disk_await.${name}`,
|
|
context: 'disk.await',
|
|
ts,
|
|
values: {
|
|
reads: dReads > 0 ? dReadMs / dReads : 0,
|
|
writes: dWrites > 0 ? dWriteMs / dWrites : 0,
|
|
},
|
|
})
|
|
batch.push({
|
|
chart: `disk_avgsz.${name}`,
|
|
context: 'disk.avgsz',
|
|
ts,
|
|
values: {
|
|
reads: dReads > 0 ? (dReadSec * 512) / dReads / 1024 : 0,
|
|
writes: dWrites > 0 ? (dWriteSec * 512) / dWrites / 1024 : 0,
|
|
},
|
|
})
|
|
batch.push({
|
|
chart: `disk_qops.${name}`,
|
|
context: 'disk.qops',
|
|
ts,
|
|
values: { operations: d.ioInFlight || 0 },
|
|
})
|
|
batch.push({
|
|
chart: `disk_busy.${name}`,
|
|
context: 'disk.busy',
|
|
ts,
|
|
values: { busy: prev ? rate(prev.ioMs, d.ioMs, dtSec) : 0 },
|
|
})
|
|
batch.push({
|
|
chart: `disk_iotime.${name}`,
|
|
context: 'disk.iotime',
|
|
ts,
|
|
values: {
|
|
reads: prev ? rate(prev.readMs, d.readMs, dtSec) : 0,
|
|
writes: prev ? rate(prev.writeMs, d.writeMs, dtSec) : 0,
|
|
},
|
|
})
|
|
if (d.discards != null) {
|
|
batch.push({
|
|
chart: `disk_discard.${name}`,
|
|
context: 'disk.discard',
|
|
ts,
|
|
values: {
|
|
operations: prev ? rate(prev.discards || 0, d.discards, dtSec) : 0,
|
|
sectors: prev ? rate(prev.discardSectors || 0, d.discardSectors, dtSec) : 0,
|
|
},
|
|
})
|
|
}
|
|
const ops = dReads + dWrites
|
|
const busyDelta = prev ? Math.max(0, d.ioMs - prev.ioMs) : 0
|
|
batch.push({
|
|
chart: `disk_svctm.${name}`,
|
|
context: 'disk.svctm',
|
|
ts,
|
|
values: { svctm: ops > 0 ? busyDelta / ops : 0 },
|
|
})
|
|
batch.push({
|
|
chart: `disk_merged.${name}`,
|
|
context: 'disk.merged',
|
|
ts,
|
|
values: {
|
|
reads: prev ? rate(prev.readMerged || 0, d.readMerged || 0, dtSec) : 0,
|
|
writes: prev ? rate(prev.writeMerged || 0, d.writeMerged || 0, dtSec) : 0,
|
|
},
|
|
})
|
|
batch.push({
|
|
chart: `disk_flush.${name}`,
|
|
context: 'disk.flush',
|
|
ts,
|
|
values: {
|
|
operations: prev ? rate(prev.flushes || 0, d.flushes || 0, dtSec) : 0,
|
|
},
|
|
})
|
|
disks[name] = { ...d, readBytes, writeBytes }
|
|
}
|
|
|
|
const prevAgg = this.lastDisk?.__agg
|
|
batch.push({
|
|
chart: 'system.io',
|
|
context: 'system.io',
|
|
ts,
|
|
values: {
|
|
in: prevAgg ? rate(prevAgg.read, aggRead, dtSec) / 1024 : 0,
|
|
out: prevAgg ? rate(prevAgg.write, aggWrite, dtSec) / 1024 : 0,
|
|
},
|
|
})
|
|
disks.__agg = { read: aggRead, write: aggWrite }
|
|
this.lastDisk = disks
|
|
}
|
|
|
|
_collectNet(batch, ts, dtSec) {
|
|
const ifaces = parseNetDev()
|
|
let rx = 0
|
|
let tx = 0
|
|
const prevAll = this.lastNet || {}
|
|
|
|
for (const [name, n] of Object.entries(ifaces)) {
|
|
if (name === '__agg') continue
|
|
const registeredIfaces = this._registeredIfaces || (this._registeredIfaces = new Set())
|
|
if (!registeredIfaces.has(name)) {
|
|
registerChart(makeNetChart(name))
|
|
registerChart(makeNetPacketsChart(name))
|
|
registerChart(makeNetErrorsChart(name))
|
|
registerChart(makeNetDropsChart(name))
|
|
registerChart(makeNetSpeedChart(name))
|
|
registerChart(makeNetDuplexChart(name))
|
|
registerChart(makeNetMtuChart(name))
|
|
registerChart(makeNetQueueChart(name))
|
|
registeredIfaces.add(name)
|
|
}
|
|
const prev = prevAll[name]
|
|
rx += n.rxBytes
|
|
tx += n.txBytes
|
|
batch.push({
|
|
chart: `net.${name}`,
|
|
context: 'net.net',
|
|
ts,
|
|
values: {
|
|
received: prev ? (rate(prev.rxBytes, n.rxBytes, dtSec) * 8) / 1000 : 0,
|
|
sent: prev ? (rate(prev.txBytes, n.txBytes, dtSec) * 8) / 1000 : 0,
|
|
},
|
|
})
|
|
batch.push({
|
|
chart: `net_packets.${name}`,
|
|
context: 'net.packets',
|
|
ts,
|
|
values: {
|
|
received: prev ? rate(prev.rxPackets, n.rxPackets, dtSec) : 0,
|
|
sent: prev ? rate(prev.txPackets, n.txPackets, dtSec) : 0,
|
|
multicast: prev ? rate(prev.rxMulti, n.rxMulti, dtSec) : 0,
|
|
},
|
|
})
|
|
batch.push({
|
|
chart: `net_errors.${name}`,
|
|
context: 'net.errors',
|
|
ts,
|
|
values: {
|
|
inbound: prev ? rate(prev.rxErrs, n.rxErrs, dtSec) : 0,
|
|
outbound: prev ? rate(prev.txErrs, n.txErrs, dtSec) : 0,
|
|
},
|
|
})
|
|
batch.push({
|
|
chart: `net_drops.${name}`,
|
|
context: 'net.drops',
|
|
ts,
|
|
values: {
|
|
inbound: prev ? rate(prev.rxDrop, n.rxDrop, dtSec) : 0,
|
|
outbound: prev ? rate(prev.txDrop, n.txDrop, dtSec) : 0,
|
|
},
|
|
})
|
|
const speed = ifaceSpeedKbps(name, this.deepState)
|
|
if (speed != null) {
|
|
batch.push({
|
|
chart: `net_speed.${name}`,
|
|
context: 'net.speed',
|
|
ts,
|
|
values: { speed },
|
|
})
|
|
}
|
|
const link = ifaceLinkMeta(name, this.deepState)
|
|
if (link.duplex) {
|
|
batch.push({
|
|
chart: `net_duplex.${name}`,
|
|
context: 'net.duplex',
|
|
ts,
|
|
values: { full: link.duplex === 'full' ? 1 : 0, half: link.duplex === 'half' ? 1 : 0 },
|
|
})
|
|
}
|
|
if (link.mtu > 0) {
|
|
batch.push({
|
|
chart: `net_mtu.${name}`,
|
|
context: 'net.mtu',
|
|
ts,
|
|
values: { mtu: link.mtu },
|
|
})
|
|
}
|
|
batch.push({
|
|
chart: `net_queue.${name}`,
|
|
context: 'net.queue_length',
|
|
ts,
|
|
values: { tx_queue_len: link.qlen },
|
|
})
|
|
}
|
|
|
|
const prevAgg = this.lastNet?.__agg
|
|
batch.push({
|
|
chart: 'system.net',
|
|
context: 'system.net',
|
|
ts,
|
|
values: {
|
|
received: prevAgg ? (rate(prevAgg.rx, rx, dtSec) * 8) / 1000 : 0,
|
|
sent: prevAgg ? (rate(prevAgg.tx, tx, dtSec) * 8) / 1000 : 0,
|
|
},
|
|
})
|
|
ifaces.__agg = { rx, tx }
|
|
this.lastNet = ifaces
|
|
}
|
|
|
|
_collectIp(batch, ts, dtSec) {
|
|
const snmp = parseSnmp()
|
|
const sock = parseSockstat()
|
|
const prev = this.lastSnmp
|
|
const ip = snmp.ip
|
|
const tcp = snmp.tcp
|
|
const udp = snmp.udp
|
|
|
|
const ipRx = ip.InOctets || 0
|
|
const ipTx = ip.OutOctets || 0
|
|
batch.push({
|
|
chart: 'system.ip',
|
|
context: 'system.ip',
|
|
ts,
|
|
values: {
|
|
received: prev ? (rate(prev.ipRx || 0, ipRx, dtSec) * 8) / 1000 : 0,
|
|
sent: prev ? (rate(prev.ipTx || 0, ipTx, dtSec) * 8) / 1000 : 0,
|
|
},
|
|
})
|
|
// IPv6 octets not always in snmp; leave zeros unless /proc/net/snmp6 present
|
|
const snmp6 = readFile('/proc/net/snmp6')
|
|
let ip6Rx = 0
|
|
let ip6Tx = 0
|
|
if (snmp6) {
|
|
for (const line of snmp6.split('\n')) {
|
|
const [k, v] = line.trim().split(/\s+/)
|
|
if (k === 'Ip6InOctets') ip6Rx = Number(v) || 0
|
|
if (k === 'Ip6OutOctets') ip6Tx = Number(v) || 0
|
|
}
|
|
}
|
|
batch.push({
|
|
chart: 'system.ipv6',
|
|
context: 'system.ipv6',
|
|
ts,
|
|
values: {
|
|
received: prev ? (rate(prev.ip6Rx || 0, ip6Rx, dtSec) * 8) / 1000 : 0,
|
|
sent: prev ? (rate(prev.ip6Tx || 0, ip6Tx, dtSec) * 8) / 1000 : 0,
|
|
},
|
|
})
|
|
|
|
batch.push({
|
|
chart: 'ip.tcppackets',
|
|
context: 'ip.tcppackets',
|
|
ts,
|
|
values: {
|
|
received: prev ? rate(prev.tcpInSegs || 0, tcp.InSegs || 0, dtSec) : 0,
|
|
sent: prev ? rate(prev.tcpOutSegs || 0, tcp.OutSegs || 0, dtSec) : 0,
|
|
},
|
|
})
|
|
batch.push({
|
|
chart: 'ip.tcperrors',
|
|
context: 'ip.tcperrors',
|
|
ts,
|
|
values: {
|
|
InErrs: prev ? rate(prev.tcpInErrs || 0, tcp.InErrs || 0, dtSec) : 0,
|
|
InCsumErrors: prev ? rate(prev.tcpInCsumErrors || 0, tcp.InCsumErrors || 0, dtSec) : 0,
|
|
RetransSegs: prev ? rate(prev.tcpRetrans || 0, tcp.RetransSegs || 0, dtSec) : 0,
|
|
},
|
|
})
|
|
batch.push({
|
|
chart: 'ip.tcpopens',
|
|
context: 'ip.tcpopens',
|
|
ts,
|
|
values: {
|
|
active: prev ? rate(prev.tcpActive || 0, tcp.ActiveOpens || 0, dtSec) : 0,
|
|
passive: prev ? rate(prev.tcpPassive || 0, tcp.PassiveOpens || 0, dtSec) : 0,
|
|
},
|
|
})
|
|
batch.push({
|
|
chart: 'ip.tcpsock',
|
|
context: 'ip.tcpsock',
|
|
ts,
|
|
values: { connections: sock.tcpInuse || tcp.CurrEstab || 0 },
|
|
})
|
|
batch.push({
|
|
chart: 'ipv4.packets',
|
|
context: 'ipv4.packets',
|
|
ts,
|
|
values: {
|
|
received: prev ? rate(prev.ipInReceives || 0, ip.InReceives || 0, dtSec) : 0,
|
|
sent: prev ? rate(prev.ipOutRequests || 0, ip.OutRequests || 0, dtSec) : 0,
|
|
forwarded: prev ? rate(prev.ipForwDatagrams || 0, ip.ForwDatagrams || 0, dtSec) : 0,
|
|
delivered: prev ? rate(prev.ipInDelivers || 0, ip.InDelivers || 0, dtSec) : 0,
|
|
},
|
|
})
|
|
batch.push({
|
|
chart: 'ipv4.errors',
|
|
context: 'ipv4.errors',
|
|
ts,
|
|
values: {
|
|
InDiscards: prev ? rate(prev.ipInDiscards || 0, ip.InDiscards || 0, dtSec) : 0,
|
|
OutDiscards: prev ? rate(prev.ipOutDiscards || 0, ip.OutDiscards || 0, dtSec) : 0,
|
|
InHdrErrors: prev ? rate(prev.ipInHdrErrors || 0, ip.InHdrErrors || 0, dtSec) : 0,
|
|
OutNoRoutes: prev ? rate(prev.ipOutNoRoutes || 0, ip.OutNoRoutes || 0, dtSec) : 0,
|
|
},
|
|
})
|
|
batch.push({
|
|
chart: 'ipv4.udppackets',
|
|
context: 'ipv4.udppackets',
|
|
ts,
|
|
values: {
|
|
received: prev ? rate(prev.udpInDatagrams || 0, udp.InDatagrams || 0, dtSec) : 0,
|
|
sent: prev ? rate(prev.udpOutDatagrams || 0, udp.OutDatagrams || 0, dtSec) : 0,
|
|
},
|
|
})
|
|
batch.push({
|
|
chart: 'ipv4.udperrors',
|
|
context: 'ipv4.udperrors',
|
|
ts,
|
|
values: {
|
|
RcvbufErrors: prev ? rate(prev.udpRcvbufErrors || 0, udp.RcvbufErrors || 0, dtSec) : 0,
|
|
SndbufErrors: prev ? rate(prev.udpSndbufErrors || 0, udp.SndbufErrors || 0, dtSec) : 0,
|
|
InErrors: prev ? rate(prev.udpInErrors || 0, udp.InErrors || 0, dtSec) : 0,
|
|
NoPorts: prev ? rate(prev.udpNoPorts || 0, udp.NoPorts || 0, dtSec) : 0,
|
|
},
|
|
})
|
|
|
|
this.lastSnmp = {
|
|
ipRx,
|
|
ipTx,
|
|
ip6Rx,
|
|
ip6Tx,
|
|
tcpInSegs: tcp.InSegs || 0,
|
|
tcpOutSegs: tcp.OutSegs || 0,
|
|
tcpInErrs: tcp.InErrs || 0,
|
|
tcpInCsumErrors: tcp.InCsumErrors || 0,
|
|
tcpRetrans: tcp.RetransSegs || 0,
|
|
tcpActive: tcp.ActiveOpens || 0,
|
|
tcpPassive: tcp.PassiveOpens || 0,
|
|
ipInReceives: ip.InReceives || 0,
|
|
ipOutRequests: ip.OutRequests || 0,
|
|
ipForwDatagrams: ip.ForwDatagrams || 0,
|
|
ipInDelivers: ip.InDelivers || 0,
|
|
ipInDiscards: ip.InDiscards || 0,
|
|
ipOutDiscards: ip.OutDiscards || 0,
|
|
ipInHdrErrors: ip.InHdrErrors || 0,
|
|
ipOutNoRoutes: ip.OutNoRoutes || 0,
|
|
udpInDatagrams: udp.InDatagrams || 0,
|
|
udpOutDatagrams: udp.OutDatagrams || 0,
|
|
udpRcvbufErrors: udp.RcvbufErrors || 0,
|
|
udpSndbufErrors: udp.SndbufErrors || 0,
|
|
udpInErrors: udp.InErrors || 0,
|
|
udpNoPorts: udp.NoPorts || 0,
|
|
}
|
|
|
|
collectNetDeep(batch, ts, dtSec, this.deepState, snmp)
|
|
}
|
|
|
|
_collectPressure(batch, ts) {
|
|
for (const [kind, someChart, fullChart] of [
|
|
['cpu', 'system.cpu_some_pressure', 'system.cpu_full_pressure'],
|
|
['memory', 'system.memory_some_pressure', 'system.memory_full_pressure'],
|
|
['io', 'system.io_some_pressure', 'system.io_full_pressure'],
|
|
]) {
|
|
const p = parsePressure(kind)
|
|
if (!p) continue
|
|
batch.push({
|
|
chart: someChart,
|
|
context: someChart,
|
|
ts,
|
|
values: { some10: p.some10, some60: p.some60, some300: p.some300 },
|
|
})
|
|
if (p.full10 != null) {
|
|
batch.push({
|
|
chart: fullChart,
|
|
context: fullChart,
|
|
ts,
|
|
values: { full10: p.full10, full60: p.full60, full300: p.full300 },
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
_collectDiskSpace(batch, ts) {
|
|
if (typeof fs.statfsSync !== 'function') return
|
|
for (const m of parseMounts()) {
|
|
const id = mountId(m.path)
|
|
const sample = sampleMount(m.path)
|
|
if (!sample) continue
|
|
if (!this._chartsRegistered) {
|
|
registerChart(makeDiskSpaceChart(id, m.path))
|
|
registerChart(makeDiskInodesChart(id, m.path))
|
|
}
|
|
batch.push({
|
|
chart: `disk_space.${id}`,
|
|
context: 'disk.space',
|
|
ts,
|
|
values: sample.space,
|
|
})
|
|
batch.push({
|
|
chart: `disk_inodes.${id}`,
|
|
context: 'disk.inodes',
|
|
ts,
|
|
values: sample.inodes,
|
|
})
|
|
}
|
|
}
|
|
|
|
getLatest() {
|
|
/** @type {Record<string, Record<string, number|null>>} */
|
|
const out = {}
|
|
for (const [k, v] of this.latest) out[k] = v
|
|
return out
|
|
}
|
|
|
|
getNodeInfo(publicKeyHex, agentVersion) {
|
|
return {
|
|
nodeId: publicKeyHex?.slice(0, 16) || os.hostname(),
|
|
hostname: os.hostname(),
|
|
publicKeyHex: publicKeyHex || null,
|
|
platform: os.platform(),
|
|
arch: os.arch(),
|
|
release: os.release(),
|
|
cpus: os.cpus().length,
|
|
totalMemMiB: bytesToMiB(os.totalmem()),
|
|
agentVersion,
|
|
startedAt: Date.now() - Math.floor(safeProcessUptime() * 1000),
|
|
charts: getAllChartDefs().map((c) => c.id),
|
|
contexts: [...new Set(getAllChartDefs().map((c) => c.context))],
|
|
sampleIntervalMs: this.intervalMs,
|
|
sampleCount: this.sampleCount,
|
|
}
|
|
}
|
|
}
|
|
|
|
/** @type {MetricsCollector|null} */
|
|
let singleton = null
|
|
|
|
export function getCollector() {
|
|
if (!singleton) singleton = new MetricsCollector()
|
|
return singleton
|
|
}
|
|
|
|
/** @internal exported for unit tests */
|
|
export { cpuDeltaPct }
|