Updates
This commit is contained in:
@@ -8,14 +8,12 @@
|
|||||||
*/
|
*/
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { execFile } from 'child_process'
|
|
||||||
import { promisify } from 'util'
|
|
||||||
import { CollectorPlugin } from './plugin.js'
|
import { CollectorPlugin } from './plugin.js'
|
||||||
import { registerChart } from '../../../shared/metrics.js'
|
import { registerChart } from '../../../shared/metrics.js'
|
||||||
|
import { execFile } from '../../utils/exec.js'
|
||||||
import logger from '../../utils/logger.js'
|
import logger from '../../utils/logger.js'
|
||||||
|
|
||||||
const log = logger.child('dmcache')
|
const log = logger.child('dmcache')
|
||||||
const execFileAsync = promisify(execFile)
|
|
||||||
|
|
||||||
const CHART_STATS = {
|
const CHART_STATS = {
|
||||||
id: 'dmcache.stats',
|
id: 'dmcache.stats',
|
||||||
@@ -92,7 +90,7 @@ export async function collectDmcacheStats() {
|
|||||||
if (!devices.length) return { hits: null, misses: null }
|
if (!devices.length) return { hits: null, misses: null }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { stdout } = await execFileAsync('dmsetup', ['status'], { timeout: 3000 })
|
const { stdout } = await execFile('dmsetup', ['status'], { timeout: 3000 })
|
||||||
const parsed = parseDmsetupStatus(stdout)
|
const parsed = parseDmsetupStatus(stdout)
|
||||||
if (parsed.hits != null || parsed.misses != null) return parsed
|
if (parsed.hits != null || parsed.misses != null) return parsed
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -5,14 +5,12 @@
|
|||||||
*
|
*
|
||||||
* Charts: sensors.ipmi.temp.*, sensors.ipmi.fan.*
|
* Charts: sensors.ipmi.temp.*, sensors.ipmi.fan.*
|
||||||
*/
|
*/
|
||||||
import { execFile } from 'child_process'
|
|
||||||
import { promisify } from 'util'
|
|
||||||
import { CollectorPlugin } from './plugin.js'
|
import { CollectorPlugin } from './plugin.js'
|
||||||
import { registerChart } from '../../../shared/metrics.js'
|
import { registerChart } from '../../../shared/metrics.js'
|
||||||
|
import { execFile } from '../../utils/exec.js'
|
||||||
import logger from '../../utils/logger.js'
|
import logger from '../../utils/logger.js'
|
||||||
|
|
||||||
const log = logger.child('ipmi')
|
const log = logger.child('ipmi')
|
||||||
const execFileAsync = promisify(execFile)
|
|
||||||
|
|
||||||
export function isIpmiEnabled() {
|
export function isIpmiEnabled() {
|
||||||
const v = process.env.PEARDATA_IPMI
|
const v = process.env.PEARDATA_IPMI
|
||||||
@@ -81,7 +79,7 @@ export function parseIpmitoolSensor(stdout) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function readIpmitoolSensors(timeoutMs = 5000) {
|
export async function readIpmitoolSensors(timeoutMs = 5000) {
|
||||||
const { stdout } = await execFileAsync('ipmitool', ['sensor'], {
|
const { stdout } = await execFile('ipmitool', ['sensor'], {
|
||||||
timeout: timeoutMs,
|
timeout: timeoutMs,
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,14 +5,12 @@
|
|||||||
*
|
*
|
||||||
* Charts: gpu.util.{i}, gpu.mem.{i}, gpu.temp.{i}, gpu.power.{i}
|
* Charts: gpu.util.{i}, gpu.mem.{i}, gpu.temp.{i}, gpu.power.{i}
|
||||||
*/
|
*/
|
||||||
import { execFile } from 'child_process'
|
|
||||||
import { promisify } from 'util'
|
|
||||||
import { CollectorPlugin } from './plugin.js'
|
import { CollectorPlugin } from './plugin.js'
|
||||||
import { registerChart } from '../../../shared/metrics.js'
|
import { registerChart } from '../../../shared/metrics.js'
|
||||||
|
import { execFile } from '../../utils/exec.js'
|
||||||
import logger from '../../utils/logger.js'
|
import logger from '../../utils/logger.js'
|
||||||
|
|
||||||
const log = logger.child('nvidia')
|
const log = logger.child('nvidia')
|
||||||
const execFileAsync = promisify(execFile)
|
|
||||||
|
|
||||||
const QUERY =
|
const QUERY =
|
||||||
'index,utilization.gpu,memory.used,memory.total,temperature.gpu,power.draw'
|
'index,utilization.gpu,memory.used,memory.total,temperature.gpu,power.draw'
|
||||||
@@ -92,7 +90,7 @@ export function parseNvidiaSmiCsv(stdout) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function queryNvidiaGpus(timeoutMs = 5000) {
|
export async function queryNvidiaGpus(timeoutMs = 5000) {
|
||||||
const { stdout } = await execFileAsync(
|
const { stdout } = await execFile(
|
||||||
'nvidia-smi',
|
'nvidia-smi',
|
||||||
[`--query-gpu=${QUERY}`, '--format=csv,noheader,nounits'],
|
[`--query-gpu=${QUERY}`, '--format=csv,noheader,nounits'],
|
||||||
{ timeout: timeoutMs, encoding: 'utf8' }
|
{ timeout: timeoutMs, encoding: 'utf8' }
|
||||||
|
|||||||
@@ -5,14 +5,12 @@
|
|||||||
*
|
*
|
||||||
* Charts: smart.temp.{dev}, smart.reallocated.{dev}
|
* Charts: smart.temp.{dev}, smart.reallocated.{dev}
|
||||||
*/
|
*/
|
||||||
import { execFile } from 'child_process'
|
|
||||||
import { promisify } from 'util'
|
|
||||||
import { CollectorPlugin } from './plugin.js'
|
import { CollectorPlugin } from './plugin.js'
|
||||||
import { registerChart } from '../../../shared/metrics.js'
|
import { registerChart } from '../../../shared/metrics.js'
|
||||||
|
import { execFile } from '../../utils/exec.js'
|
||||||
import logger from '../../utils/logger.js'
|
import logger from '../../utils/logger.js'
|
||||||
|
|
||||||
const log = logger.child('smart')
|
const log = logger.child('smart')
|
||||||
const execFileAsync = promisify(execFile)
|
|
||||||
|
|
||||||
const DEVICE_TIMEOUT_MS = 2000
|
const DEVICE_TIMEOUT_MS = 2000
|
||||||
|
|
||||||
@@ -58,7 +56,7 @@ function makeSmartChart(dev, kind) {
|
|||||||
*/
|
*/
|
||||||
export async function scanSmartDevices(timeoutMs = DEVICE_TIMEOUT_MS) {
|
export async function scanSmartDevices(timeoutMs = DEVICE_TIMEOUT_MS) {
|
||||||
try {
|
try {
|
||||||
const { stdout } = await execFileAsync('smartctl', ['--scan'], {
|
const { stdout } = await execFile('smartctl', ['--scan'], {
|
||||||
timeout: timeoutMs,
|
timeout: timeoutMs,
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
})
|
})
|
||||||
@@ -80,7 +78,7 @@ export async function scanSmartDevices(timeoutMs = DEVICE_TIMEOUT_MS) {
|
|||||||
*/
|
*/
|
||||||
export async function readSmartAttributes(dev) {
|
export async function readSmartAttributes(dev) {
|
||||||
try {
|
try {
|
||||||
const { stdout } = await execFileAsync('smartctl', ['-A', '-j', dev], {
|
const { stdout } = await execFile('smartctl', ['-A', '-j', dev], {
|
||||||
timeout: DEVICE_TIMEOUT_MS,
|
timeout: DEVICE_TIMEOUT_MS,
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
})
|
})
|
||||||
@@ -98,7 +96,7 @@ export async function readSmartAttributes(dev) {
|
|||||||
return { temp: Number.isFinite(temp) ? temp : null, reallocated: Number.isFinite(reallocated) ? reallocated : null }
|
return { temp: Number.isFinite(temp) ? temp : null, reallocated: Number.isFinite(reallocated) ? reallocated : null }
|
||||||
} catch {
|
} catch {
|
||||||
try {
|
try {
|
||||||
const { stdout } = await execFileAsync('smartctl', ['-A', dev], {
|
const { stdout } = await execFile('smartctl', ['-A', dev], {
|
||||||
timeout: DEVICE_TIMEOUT_MS,
|
timeout: DEVICE_TIMEOUT_MS,
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
* Unbound DNS collector via unbound-control or remote HTTP stats.
|
* Unbound DNS collector via unbound-control or remote HTTP stats.
|
||||||
* Enable: PEARDATA_UNBOUND=1
|
* Enable: PEARDATA_UNBOUND=1
|
||||||
*/
|
*/
|
||||||
import { spawnSync } from 'child_process'
|
|
||||||
import { CollectorPlugin } from './plugin.js'
|
import { CollectorPlugin } from './plugin.js'
|
||||||
import { registerChart } from '../../../shared/metrics.js'
|
import { registerChart } from '../../../shared/metrics.js'
|
||||||
|
import { execFileSync } from '../../utils/exec.js'
|
||||||
|
|
||||||
export function isUnboundEnabled() {
|
export function isUnboundEnabled() {
|
||||||
const v = process.env.PEARDATA_UNBOUND
|
const v = process.env.PEARDATA_UNBOUND
|
||||||
@@ -40,15 +40,16 @@ export class UnboundCollector extends CollectorPlugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!Object.keys(stats).length) {
|
if (!Object.keys(stats).length) {
|
||||||
const res = spawnSync('unbound-control', ['stats_noreset'], {
|
try {
|
||||||
|
const { stdout } = execFileSync('unbound-control', ['stats_noreset'], {
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
timeout: 2000,
|
|
||||||
})
|
})
|
||||||
if (res.status === 0 && res.stdout) {
|
for (const line of String(stdout || '').split('\n')) {
|
||||||
for (const line of res.stdout.split('\n')) {
|
|
||||||
const [k, v] = line.split('=')
|
const [k, v] = line.split('=')
|
||||||
if (k && v != null && Number.isFinite(Number(v))) stats[k.trim()] = Number(v)
|
if (k && v != null && Number.isFinite(Number(v))) stats[k.trim()] = Number(v)
|
||||||
}
|
}
|
||||||
|
} catch {
|
||||||
|
// unbound-control unavailable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
registerChart({
|
registerChart({
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { spawnSync } from 'child_process'
|
import { execFileSync } from '../../utils/exec.js'
|
||||||
import { CollectorPlugin } from './plugin.js'
|
import { CollectorPlugin } from './plugin.js'
|
||||||
import { registerChart } from '../../../shared/metrics.js'
|
import { registerChart } from '../../../shared/metrics.js'
|
||||||
|
|
||||||
@@ -106,12 +106,13 @@ export class ZfsCollector extends CollectorPlugin {
|
|||||||
|
|
||||||
// zpool list -Hp
|
// zpool list -Hp
|
||||||
try {
|
try {
|
||||||
const res = spawnSync('zpool', ['list', '-Hp', '-o', 'name,size,alloc,free,health'], {
|
const { stdout } = execFileSync(
|
||||||
encoding: 'utf8',
|
'zpool',
|
||||||
timeout: 2000,
|
['list', '-Hp', '-o', 'name,size,alloc,free,health'],
|
||||||
})
|
{ encoding: 'utf8' }
|
||||||
if (res.status === 0 && res.stdout) {
|
)
|
||||||
for (const line of res.stdout.trim().split('\n')) {
|
if (stdout) {
|
||||||
|
for (const line of String(stdout).trim().split('\n')) {
|
||||||
const [name, size, alloc, free, health] = line.split('\t')
|
const [name, size, alloc, free, health] = line.split('\t')
|
||||||
if (!name) continue
|
if (!name) continue
|
||||||
const id = `zfs.pool.${name.replace(/[^\w.-]/g, '_')}`
|
const id = `zfs.pool.${name.replace(/[^\w.-]/g, '_')}`
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
/**
|
||||||
|
* Bare-compatible process helpers.
|
||||||
|
*
|
||||||
|
* bare-subprocess (mapped from `child_process` in Bare) provides spawn /
|
||||||
|
* spawnSync only — not Node's execFile. Collectors should use this module.
|
||||||
|
*/
|
||||||
|
import { spawn, spawnSync } from 'child_process'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {unknown} buf
|
||||||
|
* @param {BufferEncoding|'buffer'|undefined} encoding
|
||||||
|
*/
|
||||||
|
function decode(buf, encoding = 'utf8') {
|
||||||
|
if (buf == null) return encoding === 'buffer' ? Buffer.alloc(0) : ''
|
||||||
|
if (encoding === 'buffer') {
|
||||||
|
return Buffer.isBuffer(buf) ? buf : Buffer.from(buf)
|
||||||
|
}
|
||||||
|
if (typeof buf === 'string') return buf
|
||||||
|
return Buffer.from(buf).toString(encoding || 'utf8')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a command and return stdout/stderr (Promise), Bare-safe.
|
||||||
|
*
|
||||||
|
* @param {string} file
|
||||||
|
* @param {string[]} [args]
|
||||||
|
* @param {{
|
||||||
|
* cwd?: string,
|
||||||
|
* env?: NodeJS.ProcessEnv,
|
||||||
|
* timeout?: number,
|
||||||
|
* encoding?: BufferEncoding|'buffer',
|
||||||
|
* maxBuffer?: number,
|
||||||
|
* }} [opts]
|
||||||
|
* @returns {Promise<{ stdout: string|Buffer, stderr: string|Buffer }>}
|
||||||
|
*/
|
||||||
|
export function execFile(file, args = [], opts = {}) {
|
||||||
|
const encoding = opts.encoding ?? 'utf8'
|
||||||
|
const timeout = Number(opts.timeout) || 0
|
||||||
|
const maxBuffer = Number(opts.maxBuffer) || 4 * 1024 * 1024
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let child
|
||||||
|
try {
|
||||||
|
child = spawn(file, args, {
|
||||||
|
cwd: opts.cwd,
|
||||||
|
env: opts.env,
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
reject(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {Buffer[]} */
|
||||||
|
const outChunks = []
|
||||||
|
/** @type {Buffer[]} */
|
||||||
|
const errChunks = []
|
||||||
|
let outLen = 0
|
||||||
|
let errLen = 0
|
||||||
|
let settled = false
|
||||||
|
/** @type {ReturnType<typeof setTimeout>|null} */
|
||||||
|
let timer = null
|
||||||
|
|
||||||
|
const settle = (err, result) => {
|
||||||
|
if (settled) return
|
||||||
|
settled = true
|
||||||
|
if (timer) clearTimeout(timer)
|
||||||
|
if (err) reject(err)
|
||||||
|
else resolve(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeout > 0) {
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
try {
|
||||||
|
child.kill()
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
const err = new Error(`Command timed out: ${file}`)
|
||||||
|
err.killed = true
|
||||||
|
settle(err)
|
||||||
|
}, timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onChunk = (chunks, which) => (chunk) => {
|
||||||
|
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
|
||||||
|
if (which === 'out') {
|
||||||
|
outLen += buf.length
|
||||||
|
if (outLen > maxBuffer) {
|
||||||
|
try {
|
||||||
|
child.kill()
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
settle(new Error(`stdout maxBuffer exceeded: ${file}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
errLen += buf.length
|
||||||
|
if (errLen > maxBuffer) {
|
||||||
|
try {
|
||||||
|
child.kill()
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
settle(new Error(`stderr maxBuffer exceeded: ${file}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chunks.push(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (child.stdout) child.stdout.on('data', onChunk(outChunks, 'out'))
|
||||||
|
if (child.stderr) child.stderr.on('data', onChunk(errChunks, 'err'))
|
||||||
|
child.on('error', (err) => settle(err))
|
||||||
|
child.on('close', (code, signal) => {
|
||||||
|
const stdout = decode(Buffer.concat(outChunks), encoding)
|
||||||
|
const stderr = decode(Buffer.concat(errChunks), encoding)
|
||||||
|
if (code && code !== 0) {
|
||||||
|
const err = new Error(`Command failed: ${file} ${args.join(' ')}`)
|
||||||
|
err.status = code
|
||||||
|
err.code = code
|
||||||
|
err.signal = signal
|
||||||
|
err.stdout = stdout
|
||||||
|
err.stderr = stderr
|
||||||
|
settle(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
settle(null, { stdout, stderr })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronous helper for short tools (zfs/unbound-style).
|
||||||
|
*
|
||||||
|
* @param {string} file
|
||||||
|
* @param {string[]} [args]
|
||||||
|
* @param {{ cwd?: string, env?: NodeJS.ProcessEnv, encoding?: BufferEncoding|'buffer', maxBuffer?: number }} [opts]
|
||||||
|
*/
|
||||||
|
export function execFileSync(file, args = [], opts = {}) {
|
||||||
|
const encoding = opts.encoding ?? 'utf8'
|
||||||
|
const res = spawnSync(file, args, {
|
||||||
|
cwd: opts.cwd,
|
||||||
|
env: opts.env,
|
||||||
|
maxBuffer: opts.maxBuffer || 4 * 1024 * 1024,
|
||||||
|
})
|
||||||
|
if (res.error) throw res.error
|
||||||
|
const stdout = decode(res.stdout, encoding)
|
||||||
|
const stderr = decode(res.stderr, encoding)
|
||||||
|
if (res.status && res.status !== 0) {
|
||||||
|
const err = new Error(`Command failed: ${file} ${args.join(' ')}`)
|
||||||
|
err.status = res.status
|
||||||
|
err.code = res.status
|
||||||
|
err.signal = res.signal
|
||||||
|
err.stdout = stdout
|
||||||
|
err.stderr = stderr
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
return { stdout, stderr, status: res.status ?? 0 }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user