Files
peardata/server/services/collectors/zfs.js
T
Raven Scott 52a1823469
CI / test (push) Successful in 1m4s
Release rolling / release (push) Successful in 7m14s
Updates
2026-07-18 20:01:47 -04:00

173 lines
4.9 KiB
JavaScript

/**
* ZFS / ZFS-on-Linux collector via /proc/spl/kstat/zfs and zpool list.
* Enable: PEARDATA_ZFS=1 (auto when kstat present and unset)
*/
import fs from 'fs'
import path from 'path'
import { execFileSync } from '../../utils/exec.js'
import { CollectorPlugin } from './plugin.js'
import { registerChart } from '../../../shared/metrics.js'
export function isZfsEnabled() {
const v = process.env.PEARDATA_ZFS
if (v === '0' || v === 'off' || v === 'false') return false
if (v === '1' || v === 'on' || v === 'true') return true
return fs.existsSync('/proc/spl/kstat/zfs')
}
function readArcstats() {
const p = '/proc/spl/kstat/zfs/arcstats'
if (!fs.existsSync(p)) return null
/** @type {Record<string, number>} */
const out = {}
for (const line of fs.readFileSync(p, 'utf8').split('\n')) {
const parts = line.trim().split(/\s+/)
if (parts.length >= 3 && /^\d+$/.test(parts[2])) out[parts[0]] = Number(parts[2])
}
return out
}
export class ZfsCollector extends CollectorPlugin {
constructor() {
super({ name: 'zfs' })
this.prev = null
this.prevTs = 0
}
isEnabled() {
return isZfsEnabled()
}
async collect() {
const ts = Date.now()
const arc = readArcstats()
/** @type {Array<{ chart: string, context: string, ts: number, values: object }>} */
const batch = []
if (arc) {
registerChart({
id: 'zfs.arc',
name: 'zfs.arc',
context: 'zfs.arc',
title: 'ZFS ARC size',
units: 'MiB',
family: 'zfs',
chartType: 'stacked',
priority: 4600,
plugin: 'zfs',
dimensions: [
{ id: 'size', name: 'size', algorithm: 'absolute' },
{ id: 'target', name: 'target', algorithm: 'absolute' },
{ id: 'mru', name: 'mru', algorithm: 'absolute' },
{ id: 'mfu', name: 'mfu', algorithm: 'absolute' },
],
})
registerChart({
id: 'zfs.arc_hits',
name: 'zfs.arc_hits',
context: 'zfs.arc_hits',
title: 'ZFS ARC hits/misses',
units: 'events/s',
family: 'zfs',
chartType: 'line',
priority: 4610,
plugin: 'zfs',
dimensions: [
{ id: 'hits', name: 'hits', algorithm: 'incremental' },
{ id: 'misses', name: 'misses', algorithm: 'incremental' },
],
})
const dt = this.prevTs ? (ts - this.prevTs) / 1000 : 0
const prev = this.prev
const mib = (n) => (n || 0) / (1024 * 1024)
batch.push({
chart: 'zfs.arc',
context: 'zfs.arc',
ts,
values: {
size: mib(arc.size),
target: mib(arc.c),
mru: mib(arc.mru_size),
mfu: mib(arc.mfu_size),
},
})
batch.push({
chart: 'zfs.arc_hits',
context: 'zfs.arc_hits',
ts,
values: {
hits: prev && dt > 0 ? Math.max(0, (arc.hits || 0) - (prev.hits || 0)) / dt : 0,
misses:
prev && dt > 0 ? Math.max(0, (arc.misses || 0) - (prev.misses || 0)) / dt : 0,
},
})
this.prev = arc
this.prevTs = ts
}
// zpool list -Hp
try {
const { stdout } = execFileSync(
'zpool',
['list', '-Hp', '-o', 'name,size,alloc,free,health'],
{ encoding: 'utf8' }
)
if (stdout) {
for (const line of String(stdout).trim().split('\n')) {
const [name, size, alloc, free, health] = line.split('\t')
if (!name) continue
const id = `zfs.pool.${name.replace(/[^\w.-]/g, '_')}`
registerChart({
id,
name: id,
context: 'zfs.pool',
title: `ZFS pool ${name}`,
units: 'GiB',
family: name,
chartType: 'stacked',
priority: 4620,
plugin: 'zfs',
dimensions: [
{ id: 'alloc', name: 'alloc', algorithm: 'absolute' },
{ id: 'free', name: 'free', algorithm: 'absolute' },
{ id: 'online', name: 'online', algorithm: 'absolute' },
],
})
batch.push({
chart: id,
context: 'zfs.pool',
ts,
values: {
alloc: Number(alloc) / (1024 ** 3),
free: Number(free) / (1024 ** 3),
online: String(health).toUpperCase() === 'ONLINE' ? 1 : 0,
},
})
void size
}
}
} catch {
// zpool not installed
}
// Also scan /proc/spl/kstat/zfs/*/io
try {
const root = '/proc/spl/kstat/zfs'
for (const ent of fs.readdirSync(root)) {
const ioPath = path.join(root, ent, 'io')
if (!fs.existsSync(ioPath)) continue
// pool-level io kstat — skip if not file
}
} catch {
// ignore
}
return batch
}
}
let singleton = null
export function getZfsCollector() {
if (!singleton) singleton = new ZfsCollector()
return singleton
}