Files
peardata/server/pipeline.js
T
Raven Scott 26298fb8d4
CI / test (push) Successful in 59s
Release rolling / release (push) Successful in 7m20s
More info
2026-07-21 13:32:27 -04:00

522 lines
14 KiB
JavaScript

/**
* Agent data pipeline: collector → memory store → anomaly → push
* ↘ warm HyperDB flush
* (+ optional Docker collector)
*/
import os from 'os'
import { getCollector } from './services/collector.js'
import {
getDockerCollector,
isDockerCollectorEnabled,
} from './services/collectors/docker.js'
import {
getProcessCollector,
isProcessCollectorEnabled,
} from './services/collectors/processes.js'
import {
getParentCollector,
isParentEnabled,
} from './services/collectors/parent.js'
import {
getNginxCollector,
isNginxEnabled,
} from './services/collectors/nginx.js'
import {
getRedisCollector,
isRedisEnabled,
} from './services/collectors/redis.js'
import {
getPostgresCollector,
isPostgresEnabled,
} from './services/collectors/postgres.js'
import {
getPearDockCollector,
isPearDockEnabled,
} from './services/collectors/peardock.js'
import {
getCgroupsCollector,
isCgroupsEnabled,
} from './services/collectors/cgroups.js'
import {
getSensorsCollector,
isSensorsEnabled,
} from './services/collectors/sensors.js'
import {
getMysqlCollector,
isMysqlEnabled,
} from './services/collectors/mysql.js'
import {
getApacheCollector,
isApacheEnabled,
} from './services/collectors/apache.js'
import {
getMemcachedCollector,
isMemcachedEnabled,
} from './services/collectors/memcached.js'
import {
getPrometheusCollector,
isPrometheusEnabled,
} from './services/collectors/prometheus.js'
import {
getMdstatCollector,
isMdstatEnabled,
} from './services/collectors/mdstat.js'
import {
getSmartCollector,
isSmartEnabled,
} from './services/collectors/smart.js'
import {
getNvidiaCollector,
isNvidiaEnabled,
} from './services/collectors/nvidia.js'
import {
getIpmiCollector,
isIpmiEnabled,
} from './services/collectors/ipmi.js'
import {
getSelfMonitorCollector,
isSelfMonitorEnabled,
} from './services/collectors/self-monitor.js'
import {
getStatsdCollector,
isStatsdEnabled,
} from './services/collectors/statsd.js'
import {
getRabbitmqCollector,
isRabbitmqEnabled,
} from './services/collectors/rabbitmq.js'
import {
getMongodbCollector,
isMongodbEnabled,
} from './services/collectors/mongodb.js'
import {
getEbpfCollector,
isEbpfEnabled,
} from './services/collectors/ebpf.js'
import {
getIopingCollector,
isIopingEnabled,
} from './services/collectors/ioping.js'
import {
getZfsCollector,
isZfsEnabled,
} from './services/collectors/zfs.js'
import {
getBcacheCollector,
isBcacheEnabled,
} from './services/collectors/bcache.js'
import {
getKafkaCollector,
isKafkaEnabled,
} from './services/collectors/kafka.js'
import {
getNatsCollector,
isNatsEnabled,
} from './services/collectors/nats.js'
import {
getUnboundCollector,
isUnboundEnabled,
} from './services/collectors/unbound.js'
import {
getSocketsCollector,
isSocketsEnabled,
} from './services/collectors/sockets.js'
import {
getDmcacheCollector,
isDmcacheEnabled,
} from './services/collectors/dmcache.js'
import {
getFsStatsCollector,
isFsStatsEnabled,
} from './services/collectors/fs-stats.js'
import { notifyAnomaly } from './services/notify.js'
import { getStore } from './services/store.js'
import { getAnomalyEngine } from './services/anomaly.js'
import {
broadcastMetrics,
broadcastAnomaly,
broadcastHealth,
} from './services/subscriptions.js'
import { enqueueWarmPoint, flushWarmPending, persistAlertEvent } from './services/warm-flush.js'
import { getDb } from './db/index.js'
import { Pushes } from '../shared/protocol.js'
import { peers } from './core/peer-registry.js'
import logger from './utils/logger.js'
const log = logger.child('pipeline')
let healthEvery = 0
let warmFlushEvery = 0
/**
* @param {import('./services/store.js').MetricStore} store
* @param {import('./services/anomaly.js').AnomalyEngine} anomalies
* @param {Array<{ chart: string, context: string, ts: number, values: object }>} batch
*/
function ingestBatch(store, anomalies, batch) {
const t0 = performance.now()
store.ingest(batch)
const tIngest = performance.now()
broadcastMetrics(batch)
const tBroadcast = performance.now()
const fired = anomalies.evaluate(batch)
const tAnomaly = performance.now()
for (const ev of fired) {
broadcastAnomaly(ev)
persistAlertEvent(ev)
notifyAnomaly(ev).catch(() => {})
if (!ev.cleared) {
peers.broadcast(Pushes.alert, {
id: ev.id,
status: ev.severity === 'critical' ? 'CRITICAL' : 'WARNING',
...ev,
})
}
}
healthEvery++
if (healthEvery >= 15) {
healthEvery = 0
broadcastHealth(anomalies.getHealth())
}
warmFlushEvery++
if (warmFlushEvery >= 10) {
warmFlushEvery = 0
flushWarmPending().catch(() => {})
}
const dur = performance.now() - t0
if (dur > 10) {
log.debug('ingest', {
durMs: Math.round(dur * 10) / 10,
ingestMs: Math.round((tIngest - t0) * 10) / 10,
bcastMs: Math.round((tBroadcast - tIngest) * 10) / 10,
anomalyMs: Math.round((tAnomaly - tBroadcast) * 10) / 10,
charts: batch.length,
fired: fired.length,
})
}
}
export function startPipeline() {
const collector = getCollector()
const store = getStore()
const anomalies = getAnomalyEngine(os.cpus().length)
store.on('warm', (point) => {
enqueueWarmPoint(point)
})
collector.on('samples', (batch) => ingestBatch(store, anomalies, batch))
collector.start()
let docker = null
if (isDockerCollectorEnabled()) {
docker = getDockerCollector()
docker.on('samples', (batch) => ingestBatch(store, anomalies, batch))
docker.start()
}
let processes = null
if (isProcessCollectorEnabled()) {
processes = getProcessCollector()
processes.on('samples', (batch) => ingestBatch(store, anomalies, batch))
processes.start()
}
let parent = null
if (isParentEnabled()) {
parent = getParentCollector()
parent.on('samples', (batch) => ingestBatch(store, anomalies, batch))
parent.start()
}
let nginx = null
if (isNginxEnabled()) {
nginx = getNginxCollector()
nginx.on('samples', (batch) => ingestBatch(store, anomalies, batch))
nginx.on('error', (err) => log.warn('Nginx collector error', { error: err.message }))
nginx.start()
}
let redis = null
if (isRedisEnabled()) {
redis = getRedisCollector()
redis.on('samples', (batch) => ingestBatch(store, anomalies, batch))
redis.on('error', (err) => log.warn('Redis collector error', { error: err.message }))
redis.start()
}
let postgres = null
if (isPostgresEnabled()) {
postgres = getPostgresCollector()
postgres.on('samples', (batch) => ingestBatch(store, anomalies, batch))
postgres.on('error', (err) => log.warn('Postgres collector error', { error: err.message }))
postgres.start()
}
let peardock = null
if (isPearDockEnabled()) {
peardock = getPearDockCollector()
peardock.on('samples', (batch) => ingestBatch(store, anomalies, batch))
peardock.start()
}
let cgroups = null
if (isCgroupsEnabled()) {
cgroups = getCgroupsCollector()
cgroups.on('samples', (batch) => ingestBatch(store, anomalies, batch))
cgroups.start()
}
let sensors = null
if (isSensorsEnabled()) {
sensors = getSensorsCollector()
sensors.on('samples', (batch) => ingestBatch(store, anomalies, batch))
sensors.start()
}
let mysql = null
if (isMysqlEnabled()) {
mysql = getMysqlCollector()
mysql.on('samples', (batch) => ingestBatch(store, anomalies, batch))
mysql.on('error', (err) => log.warn('MySQL collector error', { error: err.message }))
mysql.start()
}
let apache = null
if (isApacheEnabled()) {
apache = getApacheCollector()
apache.on('samples', (batch) => ingestBatch(store, anomalies, batch))
apache.on('error', (err) => log.warn('Apache collector error', { error: err.message }))
apache.start()
}
let memcached = null
if (isMemcachedEnabled()) {
memcached = getMemcachedCollector()
memcached.on('samples', (batch) => ingestBatch(store, anomalies, batch))
memcached.on('error', (err) => log.warn('Memcached collector error', { error: err.message }))
memcached.start()
}
let prometheus = null
if (isPrometheusEnabled()) {
prometheus = getPrometheusCollector()
prometheus.on('samples', (batch) => ingestBatch(store, anomalies, batch))
prometheus.on('error', (err) => log.warn('Prometheus collector error', { error: err.message }))
prometheus.start()
}
let mdstat = null
if (isMdstatEnabled()) {
mdstat = getMdstatCollector()
mdstat.on('samples', (batch) => ingestBatch(store, anomalies, batch))
mdstat.start()
}
let smart = null
if (isSmartEnabled()) {
smart = getSmartCollector()
smart.on('samples', (batch) => ingestBatch(store, anomalies, batch))
smart.on('error', (err) => log.warn('SMART collector error', { error: err.message }))
smart.start()
}
let nvidia = null
if (isNvidiaEnabled()) {
nvidia = getNvidiaCollector()
nvidia.on('samples', (batch) => ingestBatch(store, anomalies, batch))
nvidia.on('error', (err) => log.warn('NVIDIA collector error', { error: err.message }))
nvidia.start()
}
let ipmi = null
if (isIpmiEnabled()) {
ipmi = getIpmiCollector()
ipmi.on('samples', (batch) => ingestBatch(store, anomalies, batch))
ipmi.on('error', (err) => log.warn('IPMI collector error', { error: err.message }))
ipmi.start()
}
let selfMonitor = null
if (isSelfMonitorEnabled()) {
selfMonitor = getSelfMonitorCollector()
selfMonitor.on('samples', (batch) => ingestBatch(store, anomalies, batch))
selfMonitor.on('error', (err) => log.warn('Self-monitor error', { error: err.message }))
selfMonitor.start()
}
let statsd = null
if (isStatsdEnabled()) {
statsd = getStatsdCollector()
statsd.on('samples', (batch) => ingestBatch(store, anomalies, batch))
statsd.start()
}
let rabbitmq = null
if (isRabbitmqEnabled()) {
rabbitmq = getRabbitmqCollector()
rabbitmq.on('samples', (batch) => ingestBatch(store, anomalies, batch))
rabbitmq.on('error', (err) => log.warn('RabbitMQ collector error', { error: err.message }))
rabbitmq.start()
}
let mongodb = null
if (isMongodbEnabled()) {
mongodb = getMongodbCollector()
mongodb.on('samples', (batch) => ingestBatch(store, anomalies, batch))
mongodb.on('error', (err) => log.warn('MongoDB collector error', { error: err.message }))
mongodb.start()
}
let ebpf = null
if (isEbpfEnabled()) {
ebpf = getEbpfCollector()
ebpf.on('samples', (batch) => ingestBatch(store, anomalies, batch))
ebpf.start()
}
let ioping = null
if (isIopingEnabled()) {
ioping = getIopingCollector()
ioping.on('samples', (batch) => ingestBatch(store, anomalies, batch))
ioping.on('error', (err) => log.warn('ioping error', { error: err.message }))
ioping.start()
}
let zfs = null
if (isZfsEnabled()) {
zfs = getZfsCollector()
zfs.on('samples', (batch) => ingestBatch(store, anomalies, batch))
zfs.on('error', (err) => log.warn('ZFS error', { error: err.message }))
zfs.start()
}
let bcache = null
if (isBcacheEnabled()) {
bcache = getBcacheCollector()
bcache.on('samples', (batch) => ingestBatch(store, anomalies, batch))
bcache.on('error', (err) => log.warn('BCache error', { error: err.message }))
bcache.start()
}
let kafka = null
if (isKafkaEnabled()) {
kafka = getKafkaCollector()
kafka.on('samples', (batch) => ingestBatch(store, anomalies, batch))
kafka.on('error', (err) => log.warn('Kafka error', { error: err.message }))
kafka.start()
}
let nats = null
if (isNatsEnabled()) {
nats = getNatsCollector()
nats.on('samples', (batch) => ingestBatch(store, anomalies, batch))
nats.on('error', (err) => log.warn('NATS error', { error: err.message }))
nats.start()
}
let unbound = null
if (isUnboundEnabled()) {
unbound = getUnboundCollector()
unbound.on('samples', (batch) => ingestBatch(store, anomalies, batch))
unbound.on('error', (err) => log.warn('Unbound error', { error: err.message }))
unbound.start()
}
let sockets = null
if (isSocketsEnabled()) {
sockets = getSocketsCollector()
sockets.on('samples', (batch) => ingestBatch(store, anomalies, batch))
sockets.start()
}
let dmcache = null
if (isDmcacheEnabled()) {
dmcache = getDmcacheCollector()
dmcache.on('samples', (batch) => ingestBatch(store, anomalies, batch))
dmcache.on('error', (err) => log.warn('dm-cache error', { error: err.message }))
dmcache.start()
}
let fsStats = null
if (isFsStatsEnabled()) {
fsStats = getFsStatsCollector()
fsStats.on('samples', (batch) => ingestBatch(store, anomalies, batch))
fsStats.on('error', (err) => log.warn('fs-stats error', { error: err.message }))
fsStats.start()
}
log.info('Metrics pipeline started', {
hyperdb: Boolean(getDb()),
docker: Boolean(docker),
processes: Boolean(processes),
parent: Boolean(parent),
nginx: Boolean(nginx),
redis: Boolean(redis),
postgres: Boolean(postgres),
peardock: Boolean(peardock),
cgroups: Boolean(cgroups),
sensors: Boolean(sensors),
mysql: Boolean(mysql),
apache: Boolean(apache),
memcached: Boolean(memcached),
prometheus: Boolean(prometheus),
mdstat: Boolean(mdstat),
smart: Boolean(smart),
nvidia: Boolean(nvidia),
ipmi: Boolean(ipmi),
selfMonitor: Boolean(selfMonitor),
statsd: Boolean(statsd),
rabbitmq: Boolean(rabbitmq),
mongodb: Boolean(mongodb),
ebpf: Boolean(ebpf),
ioping: Boolean(ioping),
zfs: Boolean(zfs),
bcache: Boolean(bcache),
kafka: Boolean(kafka),
nats: Boolean(nats),
unbound: Boolean(unbound),
sockets: Boolean(sockets),
dmcache: Boolean(dmcache),
fsStats: Boolean(fsStats),
})
return {
collector,
store,
anomalies,
docker,
processes,
parent,
nginx,
redis,
postgres,
peardock,
cgroups,
sensors,
mysql,
apache,
memcached,
prometheus,
mdstat,
smart,
nvidia,
ipmi,
selfMonitor,
statsd,
rabbitmq,
mongodb,
ebpf,
ioping,
zfs,
bcache,
kafka,
nats,
unbound,
sockets,
dmcache,
fsStats,
}
}