152 lines
3.8 KiB
JavaScript
152 lines
3.8 KiB
JavaScript
/**
|
|
* Device-mapper cache stats (dm-cache / dm-cache target).
|
|
*
|
|
* Auto when dm cache devices are present.
|
|
* Disable: PEARDATA_DMCACHE=0
|
|
*
|
|
* Charts: dmcache.stats
|
|
*/
|
|
import fs from 'fs'
|
|
import path from 'path'
|
|
import { CollectorPlugin } from './plugin.js'
|
|
import { registerChart } from '../../../shared/metrics.js'
|
|
import { execFile } from '../../utils/exec.js'
|
|
import logger from '../../utils/logger.js'
|
|
|
|
const log = logger.child('dmcache')
|
|
|
|
const CHART_STATS = {
|
|
id: 'dmcache.stats',
|
|
name: 'dmcache.stats',
|
|
context: 'dmcache.stats',
|
|
title: 'dm-cache stats',
|
|
units: 'count',
|
|
family: 'dmcache',
|
|
chartType: 'line',
|
|
priority: 4340,
|
|
plugin: 'dmcache',
|
|
dimensions: [
|
|
{ id: 'hits', name: 'hits', algorithm: 'absolute' },
|
|
{ id: 'misses', name: 'misses', algorithm: 'absolute' },
|
|
],
|
|
}
|
|
|
|
export function isDmcacheEnabled() {
|
|
const v = process.env.PEARDATA_DMCACHE
|
|
if (v === '0' || v === 'off' || v === 'false') return false
|
|
if (v === '1' || v === 'on' || v === 'true') return true
|
|
return discoverDmCacheDevices().length > 0
|
|
}
|
|
|
|
function readFile(p) {
|
|
try {
|
|
return fs.readFileSync(p, 'utf8')
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @returns {string[]}
|
|
*/
|
|
export function discoverDmCacheDevices() {
|
|
/** @type {string[]} */
|
|
const found = []
|
|
const blockRoot = '/sys/block'
|
|
if (!fs.existsSync(blockRoot)) return found
|
|
for (const name of fs.readdirSync(blockRoot)) {
|
|
if (!name.startsWith('dm-')) continue
|
|
const dmName = readFile(path.join(blockRoot, name, 'dm', 'name'))
|
|
if (!dmName) continue
|
|
const lower = dmName.toLowerCase()
|
|
if (lower.includes('cache') || lower.includes('dmcache')) {
|
|
found.push(name)
|
|
}
|
|
}
|
|
return found
|
|
}
|
|
|
|
/**
|
|
* @param {string} output
|
|
* @returns {{ hits: number|null, misses: number|null }}
|
|
*/
|
|
export function parseDmsetupStatus(output) {
|
|
let hits = null
|
|
let misses = null
|
|
for (const line of String(output || '').split('\n')) {
|
|
const h = line.match(/(\d+)\s+hits?/i)
|
|
const m = line.match(/(\d+)\s+miss(?:es)?/i)
|
|
if (h) hits = (hits ?? 0) + Number(h[1])
|
|
if (m) misses = (misses ?? 0) + Number(m[1])
|
|
}
|
|
return { hits, misses }
|
|
}
|
|
|
|
/**
|
|
* @returns {Promise<{ hits: number|null, misses: number|null }>}
|
|
*/
|
|
export async function collectDmcacheStats() {
|
|
const devices = discoverDmCacheDevices()
|
|
if (!devices.length) return { hits: null, misses: null }
|
|
|
|
try {
|
|
const { stdout } = await execFile('dmsetup', ['status'], { timeout: 3000 })
|
|
const parsed = parseDmsetupStatus(stdout)
|
|
if (parsed.hits != null || parsed.misses != null) return parsed
|
|
} catch {
|
|
// fall through to sysfs
|
|
}
|
|
|
|
let hits = null
|
|
let misses = null
|
|
for (const dev of devices) {
|
|
const statsDir = path.join('/sys/block', dev, 'dm', 'stats')
|
|
const raw = readFile(statsDir) || readFile(path.join('/sys/block', dev, 'stat'))
|
|
if (!raw) continue
|
|
const h = raw.match(/hits?\s+(\d+)/i)
|
|
const m = raw.match(/miss(?:es)?\s+(\d+)/i)
|
|
if (h) hits = (hits ?? 0) + Number(h[1])
|
|
if (m) misses = (misses ?? 0) + Number(m[1])
|
|
}
|
|
return { hits, misses }
|
|
}
|
|
|
|
export class DmcacheCollector extends CollectorPlugin {
|
|
constructor(opts = {}) {
|
|
super({ name: 'dmcache', intervalMs: opts.intervalMs })
|
|
}
|
|
|
|
isEnabled() {
|
|
return isDmcacheEnabled()
|
|
}
|
|
|
|
start() {
|
|
if (!this.isEnabled()) return
|
|
registerChart(CHART_STATS)
|
|
log.info('dm-cache collector started')
|
|
super.start()
|
|
}
|
|
|
|
async collect() {
|
|
const stats = await collectDmcacheStats()
|
|
if (stats.hits == null && stats.misses == null) return []
|
|
const ts = Date.now()
|
|
return [
|
|
{
|
|
chart: 'dmcache.stats',
|
|
context: 'dmcache.stats',
|
|
ts,
|
|
values: { hits: stats.hits, misses: stats.misses },
|
|
},
|
|
]
|
|
}
|
|
}
|
|
|
|
/** @type {DmcacheCollector|null} */
|
|
let singleton = null
|
|
|
|
export function getDmcacheCollector() {
|
|
if (!singleton) singleton = new DmcacheCollector()
|
|
return singleton
|
|
}
|