174 lines
4.8 KiB
JavaScript
174 lines
4.8 KiB
JavaScript
/**
|
|
* 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 { readFileBuf } from '../../utils/fd-cache.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) {
|
|
return readFileBuf(p)
|
|
}
|
|
|
|
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 t0 = performance.now()
|
|
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 },
|
|
})
|
|
}
|
|
const dur = performance.now() - t0
|
|
if (dur > 5) log.debug('tick', { collector: 'fs-stats', durMs: Math.round(dur * 10) / 10 })
|
|
return batch
|
|
}
|
|
}
|
|
|
|
/** @type {FsStatsCollector|null} */
|
|
let singleton = null
|
|
|
|
export function getFsStatsCollector() {
|
|
if (!singleton) singleton = new FsStatsCollector()
|
|
return singleton
|
|
}
|