Updates
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* 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 { spawnSync } from 'child_process'
|
||||
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 res = spawnSync('zpool', ['list', '-Hp', '-o', 'name,size,alloc,free,health'], {
|
||||
encoding: 'utf8',
|
||||
timeout: 2000,
|
||||
})
|
||||
if (res.status === 0 && res.stdout) {
|
||||
for (const line of res.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
|
||||
}
|
||||
Reference in New Issue
Block a user