Updates
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user