Updates
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
Reference in New Issue
Block a user