173 lines
4.9 KiB
JavaScript
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
|
|
}
|