Files
peardata/server/services/collectors/fs-stats.js
T
Raven Scott ac4718cb66
CI / test (push) Failing after 4s
Release rolling / release (push) Successful in 7m14s
Further CPU Experimental Changes
2026-07-21 12:17:06 -04:00

171 lines
4.6 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 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
}