/** * Unbound DNS collector via unbound-control or remote HTTP stats. * Enable: PEARDATA_UNBOUND=1 */ import { CollectorPlugin } from './plugin.js' import { registerChart } from '../../../shared/metrics.js' import { execFileSync } from '../../utils/exec.js' export function isUnboundEnabled() { const v = process.env.PEARDATA_UNBOUND return v === '1' || v === 'on' || v === 'true' } export class UnboundCollector extends CollectorPlugin { constructor() { super({ name: 'unbound' }) this.prev = null this.prevTs = 0 } isEnabled() { return isUnboundEnabled() } async collect() { const ts = Date.now() /** @type {Record} */ const stats = {} const http = process.env.PEARDATA_UNBOUND_URL if (http) { try { const res = await fetch(http, { signal: AbortSignal.timeout(2000) }) const text = await res.text() for (const line of text.split('\n')) { const [k, v] = line.split('=') if (k && v != null && Number.isFinite(Number(v))) stats[k.trim()] = Number(v) } } catch { // fall through } } if (!Object.keys(stats).length) { try { const { stdout } = execFileSync('unbound-control', ['stats_noreset'], { encoding: 'utf8', }) for (const line of String(stdout || '').split('\n')) { const [k, v] = line.split('=') if (k && v != null && Number.isFinite(Number(v))) stats[k.trim()] = Number(v) } } catch { // unbound-control unavailable } } registerChart({ id: 'unbound.up', name: 'unbound.up', context: 'unbound.up', title: 'Unbound up', units: 'boolean', family: 'unbound', chartType: 'line', priority: 8900, plugin: 'unbound', dimensions: [{ id: 'up', name: 'up', algorithm: 'absolute' }], }) if (!Object.keys(stats).length) { return [{ chart: 'unbound.up', context: 'unbound.up', ts, values: { up: 0 } }] } const dt = this.prevTs ? (ts - this.prevTs) / 1000 : 0 const prev = this.prev const num = (k) => stats[k] || 0 const rate = (k) => (prev && dt > 0 ? Math.max(0, num(k) - (prev[k] || 0)) / dt : 0) registerChart({ id: 'unbound.queries', name: 'unbound.queries', context: 'unbound.queries', title: 'Unbound queries', units: 'queries/s', family: 'unbound', chartType: 'line', priority: 8910, plugin: 'unbound', dimensions: [ { id: 'total', name: 'total', algorithm: 'incremental' }, { id: 'cache_hits', name: 'cache_hits', algorithm: 'incremental' }, { id: 'cache_misses', name: 'cache_misses', algorithm: 'incremental' }, ], }) this.prev = stats this.prevTs = ts return [ { chart: 'unbound.up', context: 'unbound.up', ts, values: { up: 1 } }, { chart: 'unbound.queries', context: 'unbound.queries', ts, values: { total: rate('total.num.queries') || rate('num.queries'), cache_hits: rate('total.num.cachehits') || rate('num.cachehits'), cache_misses: rate('total.num.cachemiss') || rate('num.cachemiss'), }, }, ] } } let singleton = null export function getUnboundCollector() { if (!singleton) singleton = new UnboundCollector() return singleton }