Files
peardata/server/services/collectors/mdstat.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

185 lines
4.8 KiB
JavaScript

/**
* Linux MD RAID /proc/mdstat helper + collector.
*
* Always-on when /proc/mdstat has active arrays.
*
* Charts: md.health, md.array.{name}
*/
import fs from 'fs'
import os from 'os'
import { EventEmitter } from 'events'
import { SAMPLE_INTERVAL_MS, registerChart } from '../../../shared/metrics.js'
import { readFileBuf } from '../../utils/fd-cache.js'
import logger from '../../utils/logger.js'
const log = logger.child('mdstat')
const CHART_HEALTH = {
id: 'md.health',
name: 'md.health',
context: 'md.health',
title: 'MD RAID arrays',
units: 'arrays',
family: 'md',
chartType: 'line',
priority: 4500,
plugin: 'mdstat',
dimensions: [
{ id: 'active', name: 'active', algorithm: 'absolute' },
{ id: 'degraded', name: 'degraded', algorithm: 'absolute' },
{ id: 'recovering', name: 'recovering', algorithm: 'absolute' },
],
}
function readFile(p) {
return readFileBuf(p)
}
/**
* @returns {boolean}
*/
export function isMdstatUseful() {
if (os.platform() !== 'linux') return false
const raw = readFile('/proc/mdstat')
if (!raw) return false
return /^md\d+\s*:/m.test(raw)
}
export function isMdstatEnabled() {
return isMdstatUseful()
}
/**
* @param {string} name
*/
function makeMdArrayChart(name) {
const safe = String(name).replace(/[^a-zA-Z0-9_.-]/g, '_').slice(0, 64)
return {
id: `md.array.${safe}`,
name: `md.array.${safe}`,
context: 'md.array',
title: `MD array ${name}`,
units: 'disks',
family: name,
chartType: 'line',
priority: 4510,
plugin: 'mdstat',
dimensions: [
{ id: 'active', name: 'active', algorithm: 'absolute' },
{ id: 'total', name: 'total', algorithm: 'absolute' },
],
}
}
/**
* @param {string} raw
* @returns {{ health: { active: number, degraded: number, recovering: number }, arrays: Array<{ name: string, active: number, total: number, degraded: boolean, recovering: boolean }> }}
*/
export function parseMdstat(raw) {
const text = String(raw || '')
/** @type {Array<{ name: string, active: number, total: number, degraded: boolean, recovering: boolean }>} */
const arrays = []
const lines = text.split('\n')
let i = 0
while (i < lines.length) {
const hdr = lines[i].match(/^(md\d+)\s*:\s*(.*)$/)
if (!hdr) {
i++
continue
}
const name = hdr[1]
const hdrRest = hdr[2] || ''
const detail = lines[i + 1] || ''
const block = `${hdrRest} ${detail}`.toLowerCase()
const bracket = detail.match(/\[(\d+)\/(\d+)\]/)
const active = bracket ? Number(bracket[1]) : 0
const total = bracket ? Number(bracket[2]) : 0
const diskState = detail.match(/\[([U_.]+)\]/)
const degraded =
/degraded|faulty|F/i.test(block) ||
(diskState ? /[_F]/.test(diskState[1]) : active < total && total > 0)
const recovering = /recovery|resync|reshape|check|repair/i.test(block)
arrays.push({ name, active, total, degraded, recovering })
i += 2
}
return {
health: {
active: arrays.length,
degraded: arrays.filter((a) => a.degraded).length,
recovering: arrays.filter((a) => a.recovering).length,
},
arrays,
}
}
/**
* Append mdstat samples to an existing batch.
* @param {Array<{ chart: string, context: string, ts: number, values: Record<string, number|null> }>} batch
* @param {number} ts
*/
export function collectMdstat(batch, ts) {
if (!isMdstatUseful()) return
const raw = readFile('/proc/mdstat')
if (!raw) return
const parsed = parseMdstat(raw)
registerChart(CHART_HEALTH)
batch.push({
chart: 'md.health',
context: 'md.health',
ts,
values: parsed.health,
})
for (const arr of parsed.arrays) {
const def = makeMdArrayChart(arr.name)
registerChart(def)
batch.push({
chart: def.id,
context: def.context,
ts,
values: { active: arr.active, total: arr.total },
})
}
}
export class MdstatCollector extends EventEmitter {
constructor(opts = {}) {
super()
this.intervalMs =
opts.intervalMs ?? (Number(process.env.PEARDATA_SAMPLE_MS) || SAMPLE_INTERVAL_MS)
this.timer = null
this.running = false
}
start() {
if (this.running || !isMdstatUseful()) return
this.running = true
this._tick()
this.timer = setInterval(() => this._tick(), this.intervalMs)
if (this.timer.unref) this.timer.unref()
log.info('Mdstat collector started')
}
stop() {
this.running = false
if (this.timer) clearInterval(this.timer)
this.timer = null
}
_tick() {
const ts = Date.now()
/** @type {Array<{ chart: string, context: string, ts: number, values: Record<string, number|null> }>} */
const batch = []
collectMdstat(batch, ts)
if (batch.length) this.emit('samples', batch)
}
}
/** @type {MdstatCollector|null} */
let singleton = null
export function getMdstatCollector() {
if (!singleton) singleton = new MdstatCollector()
return singleton
}