130 lines
3.5 KiB
JavaScript
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
|
|
}
|