Files
peardata/server/services/collectors/unbound.js
T
Raven Scott 52a1823469
CI / test (push) Successful in 1m4s
Release rolling / release (push) Successful in 7m14s
Updates
2026-07-18 20:01:47 -04:00

113 lines
3.3 KiB
JavaScript

/**
* 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<string, number>} */
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
}