Updates
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* Filesystem lifetime read/write stats (ext4, xfs).
|
||||
*
|
||||
* Enable: default on Linux when unset
|
||||
* Disable: PEARDATA_FS_STATS=0
|
||||
*
|
||||
* Charts: fs.ext4.{dev}, fs.xfs.{dev}
|
||||
*/
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import os from 'os'
|
||||
import { CollectorPlugin } from './plugin.js'
|
||||
import { registerChart } from '../../../shared/metrics.js'
|
||||
import logger from '../../utils/logger.js'
|
||||
|
||||
const log = logger.child('fs-stats')
|
||||
|
||||
export function isFsStatsEnabled() {
|
||||
const v = process.env.PEARDATA_FS_STATS
|
||||
if (v === '0' || v === 'off' || v === 'false') return false
|
||||
if (v === '1' || v === 'on' || v === 'true') return true
|
||||
return os.platform() === 'linux'
|
||||
}
|
||||
|
||||
function readFile(p) {
|
||||
try {
|
||||
return fs.readFileSync(p, 'utf8')
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function readNum(p) {
|
||||
const raw = readFile(p)
|
||||
if (raw == null) return null
|
||||
const n = Number(String(raw).trim())
|
||||
return Number.isFinite(n) ? n : null
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
*/
|
||||
function sanitizeDev(name) {
|
||||
return String(name)
|
||||
.replace(/[^a-zA-Z0-9_.-]/g, '_')
|
||||
.replace(/^_+|_+$/g, '')
|
||||
.slice(0, 64)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {'ext4'|'xfs'} fsType
|
||||
* @param {string} dev
|
||||
*/
|
||||
function makeFsChart(fsType, dev) {
|
||||
const safe = sanitizeDev(dev)
|
||||
const id = `fs.${fsType}.${safe}`
|
||||
return {
|
||||
id,
|
||||
name: id,
|
||||
context: `fs.${fsType}`,
|
||||
title: `${fsType.toUpperCase()} ${dev}`,
|
||||
units: 'kB',
|
||||
family: dev,
|
||||
chartType: 'line',
|
||||
priority: fsType === 'ext4' ? 4300 : 4310,
|
||||
plugin: 'fs-stats',
|
||||
dimensions: [
|
||||
{ id: 'write_kb', name: 'write_kb', algorithm: 'absolute' },
|
||||
{ id: 'read_kb', name: 'read_kb', algorithm: 'absolute' },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Array<{ fsType: 'ext4'|'xfs', dev: string, write_kb: number|null, read_kb: number|null }>}
|
||||
*/
|
||||
export function collectFsStatsData() {
|
||||
/** @type {Array<{ fsType: 'ext4'|'xfs', dev: string, write_kb: number|null, read_kb: number|null }>} */
|
||||
const out = []
|
||||
|
||||
const ext4Root = '/sys/fs/ext4'
|
||||
if (fs.existsSync(ext4Root)) {
|
||||
for (const dev of fs.readdirSync(ext4Root)) {
|
||||
const base = path.join(ext4Root, dev)
|
||||
let write_kb = readNum(path.join(base, 'lifetime_write_kbytes'))
|
||||
let read_kb = readNum(path.join(base, 'lifetime_read_kbytes'))
|
||||
if (write_kb == null && read_kb == null) {
|
||||
const statsPath = path.join('/proc/fs/ext4', dev, 'stats')
|
||||
const raw = readFile(statsPath)
|
||||
if (raw) {
|
||||
const nums = raw.trim().split(/\s+/).map(Number)
|
||||
if (nums.length >= 2) {
|
||||
write_kb = Number.isFinite(nums[0]) ? nums[0] : null
|
||||
read_kb = Number.isFinite(nums[1]) ? nums[1] : null
|
||||
}
|
||||
}
|
||||
}
|
||||
if (write_kb != null || read_kb != null) {
|
||||
out.push({ fsType: 'ext4', dev, write_kb, read_kb })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const xfsRoot = '/sys/fs/xfs'
|
||||
if (fs.existsSync(xfsRoot)) {
|
||||
for (const dev of fs.readdirSync(xfsRoot)) {
|
||||
const statsDir = path.join(xfsRoot, dev, 'stats')
|
||||
if (!fs.existsSync(statsDir)) continue
|
||||
let write_kb = null
|
||||
let read_kb = null
|
||||
const statsFile = path.join(statsDir, 'stats')
|
||||
const raw = readFile(statsFile)
|
||||
if (raw) {
|
||||
for (const line of raw.split('\n')) {
|
||||
const w = line.match(/write\s+(\d+)/i)
|
||||
const r = line.match(/read\s+(\d+)/i)
|
||||
if (w) write_kb = Number(w[1])
|
||||
if (r) read_kb = Number(r[1])
|
||||
}
|
||||
}
|
||||
write_kb = write_kb ?? readNum(path.join(statsDir, 'write'))
|
||||
read_kb = read_kb ?? readNum(path.join(statsDir, 'read'))
|
||||
if (write_kb != null || read_kb != null) {
|
||||
out.push({ fsType: 'xfs', dev, write_kb, read_kb })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
export class FsStatsCollector extends CollectorPlugin {
|
||||
constructor(opts = {}) {
|
||||
super({ name: 'fs-stats', intervalMs: opts.intervalMs })
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
return isFsStatsEnabled()
|
||||
}
|
||||
|
||||
start() {
|
||||
if (!this.isEnabled()) return
|
||||
log.info('Filesystem stats collector started')
|
||||
super.start()
|
||||
}
|
||||
|
||||
async collect() {
|
||||
const rows = collectFsStatsData()
|
||||
if (!rows.length) return []
|
||||
const ts = Date.now()
|
||||
/** @type {Array<{ chart: string, context: string, ts: number, values: Record<string, number|null> }>} */
|
||||
const batch = []
|
||||
for (const row of rows) {
|
||||
const def = makeFsChart(row.fsType, row.dev)
|
||||
registerChart(def)
|
||||
batch.push({
|
||||
chart: def.id,
|
||||
context: def.context,
|
||||
ts,
|
||||
values: { write_kb: row.write_kb, read_kb: row.read_kb },
|
||||
})
|
||||
}
|
||||
return batch
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {FsStatsCollector|null} */
|
||||
let singleton = null
|
||||
|
||||
export function getFsStatsCollector() {
|
||||
if (!singleton) singleton = new FsStatsCollector()
|
||||
return singleton
|
||||
}
|
||||
Reference in New Issue
Block a user