Updates
CI / test (push) Successful in 1m24s
Release rolling / release (push) Has been cancelled

This commit is contained in:
Raven Scott
2026-07-18 19:44:32 -04:00
parent 0592351da2
commit 2e1a3e9b06
36 changed files with 6930 additions and 70 deletions
+133
View File
@@ -0,0 +1,133 @@
/**
* Disk I/O latency probe via ioping or fsync fallback.
*
* Enable: PEARDATA_IOPING=1
* Path: PEARDATA_IOPING_PATH=/ (default)
*
* Charts: ioping.latency
*/
import fs from 'fs'
import path from 'path'
import os from 'os'
import { execFile } from 'child_process'
import { promisify } from 'util'
import { CollectorPlugin } from './plugin.js'
import { registerChart } from '../../../shared/metrics.js'
import logger from '../../utils/logger.js'
const log = logger.child('ioping')
const execFileAsync = promisify(execFile)
const CHART_LATENCY = {
id: 'ioping.latency',
name: 'ioping.latency',
context: 'ioping.latency',
title: 'I/O ping latency',
units: 'ms',
family: 'ioping',
chartType: 'line',
priority: 4200,
plugin: 'ioping',
dimensions: [{ id: 'latency_ms', name: 'latency_ms', algorithm: 'absolute' }],
}
export function isIopingEnabled() {
const v = process.env.PEARDATA_IOPING
return v === '1' || v === 'on' || v === 'true'
}
function targetPath() {
return process.env.PEARDATA_IOPING_PATH || '/'
}
/**
* @param {string} output
* @returns {number|null}
*/
export function parseIopingOutput(output) {
const m = String(output || '').match(/([\d.]+)\s*ms/i)
if (m) return Number(m[1])
const n = Number(String(output || '').trim().split(/\s+/).pop())
return Number.isFinite(n) ? n : null
}
/**
* @param {string} target
* @returns {Promise<number|null>}
*/
export async function probeIoping(target) {
try {
const { stdout } = await execFileAsync('ioping', ['-c', '1', '-q', target], {
timeout: 10000,
})
return parseIopingOutput(stdout)
} catch {
return null
}
}
/**
* @param {string} target
* @returns {number|null}
*/
export function probeFsyncFallback(target) {
const dir = fs.existsSync(target) && fs.statSync(target).isDirectory() ? target : os.tmpdir()
const file = path.join(dir, `.peardata-ioping-${process.pid}`)
const started = Date.now()
try {
fs.writeFileSync(file, `${Date.now()}\n`)
const fd = fs.openSync(file, 'r+')
fs.fsyncSync(fd)
fs.closeSync(fd)
return Date.now() - started
} catch {
return null
} finally {
try {
fs.unlinkSync(file)
} catch {
// ignore
}
}
}
export class IopingCollector extends CollectorPlugin {
constructor(opts = {}) {
super({ name: 'ioping', intervalMs: opts.intervalMs })
this.target = opts.path || targetPath()
}
isEnabled() {
return isIopingEnabled()
}
start() {
if (!this.isEnabled()) return
registerChart(CHART_LATENCY)
log.info('Ioping collector started', { path: this.target })
super.start()
}
async collect() {
const ts = Date.now()
let latency = await probeIoping(this.target)
if (latency == null) latency = probeFsyncFallback(this.target)
if (latency == null) return []
return [
{
chart: 'ioping.latency',
context: 'ioping.latency',
ts,
values: { latency_ms: latency },
},
]
}
}
/** @type {IopingCollector|null} */
let singleton = null
export function getIopingCollector() {
if (!singleton) singleton = new IopingCollector()
return singleton
}