/** * eBPF metrics bridge — ingests samples from an external helper. * * The agent cannot load eBPF programs in pure JS. Instead, a privileged helper * can write JSON lines to a file or unix socket path: * * PEARDATA_EBPF=1 * PEARDATA_EBPF_PATH=/run/peardata/ebpf.ndjson * * Each line: {"chart":"ebpf.cachestat","values":{"hits":1,"misses":2},"ts":...} * * Charts are registered dynamically under context ebpf.*. */ import fs from 'fs' import { EventEmitter } from 'events' import { SAMPLE_INTERVAL_MS, registerChart } from '../../../shared/metrics.js' import logger from '../../utils/logger.js' const log = logger.child('ebpf-bridge') export function isEbpfBridgeEnabled() { const v = process.env.PEARDATA_EBPF return v === '1' || v === 'on' || v === 'true' } function bridgePath() { return process.env.PEARDATA_EBPF_PATH || '/run/peardata/ebpf.ndjson' } export class EbpfBridgeCollector extends EventEmitter { constructor(opts = {}) { super() this.intervalMs = opts.intervalMs ?? (Number(process.env.PEARDATA_SAMPLE_MS) || SAMPLE_INTERVAL_MS) this.timer = null this.running = false this.offset = 0 } start() { if (this.running) return this.running = true this._tick() this.timer = setInterval(() => this._tick(), this.intervalMs) if (this.timer.unref) this.timer.unref() log.info('eBPF bridge started', { path: bridgePath() }) } stop() { this.running = false if (this.timer) clearInterval(this.timer) this.timer = null } _tick() { const p = bridgePath() let st try { st = fs.statSync(p) } catch { return } if (st.size < this.offset) this.offset = 0 if (st.size === this.offset) return let fd try { fd = fs.openSync(p, 'r') const len = st.size - this.offset const buf = Buffer.alloc(Math.min(len, 256 * 1024)) const n = fs.readSync(fd, buf, 0, buf.length, this.offset) this.offset += n const text = buf.slice(0, n).toString('utf8') const ts = Date.now() /** @type {Array<{ chart: string, context: string, ts: number, values: object }>} */ const batch = [] for (const line of text.split('\n')) { if (!line.trim()) continue let row try { row = JSON.parse(line) } catch { continue } if (!row?.chart || !row.values) continue const chart = String(row.chart).startsWith('ebpf.') ? String(row.chart) : `ebpf.${String(row.chart)}` const dims = Object.keys(row.values).map((id) => ({ id, name: id, algorithm: 'absolute', })) registerChart({ id: chart, name: chart, context: chart.split('.').slice(0, 2).join('.') || 'ebpf.metric', title: chart, units: row.units || 'events', family: 'ebpf', chartType: 'line', priority: 9500, plugin: 'ebpf-bridge', dimensions: dims, }) batch.push({ chart, context: chart, ts: row.ts || ts, values: row.values, }) } if (batch.length) this.emit('samples', batch) } catch (err) { log.warn('eBPF bridge read failed', { error: err.message }) } finally { try { if (fd != null) fs.closeSync(fd) } catch { // ignore } } } } let singleton = null export function getEbpfBridgeCollector() { if (!singleton) singleton = new EbpfBridgeCollector() return singleton }