Files
peardata/server/services/collectors/ebpf-bridge.js
T
Raven Scott 2e1a3e9b06
CI / test (push) Successful in 1m24s
Release rolling / release (push) Has been cancelled
Updates
2026-07-18 19:44:32 -04:00

130 lines
3.5 KiB
JavaScript

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