/** * 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 }>} 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 }>} */ 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 }