Hot memory — 15m → 24h (or custom points) Warm memory — downsampled ring + samples-per-bucket Disk history — auto-rotate by age (1 day → 1 year, forever, or custom days) Soft disk budget + max warm points Auto prune on a configurable hour interval Dry-run prune / Prune now / Save to agent (admin role)
258 lines
6.7 KiB
JavaScript
258 lines
6.7 KiB
JavaScript
/**
|
|
* PearData / PearMonitor agent entry point.
|
|
*
|
|
* - HyperDHT listener + protomux-rpc (P2P control + metric streams)
|
|
* - HyperDB (Corestore) for warm history + metadata
|
|
* - Optional Hyperswarm replication (PEARDATA_SWARM=1)
|
|
* - Optional agent-compatible REST API (default :18888)
|
|
*/
|
|
import os from 'os'
|
|
import DHT from 'hyperdht'
|
|
import b4a from 'b4a'
|
|
import gracefulGoodbye from 'graceful-goodbye'
|
|
import { loadOrCreateKeyPair } from './core/keys.js'
|
|
import { initAuthKeys } from './core/auth-keys.js'
|
|
import { peers } from './core/peer-registry.js'
|
|
import { PeerSession } from './rpc/session.js'
|
|
import { registerAllHandlers, cleanupSession } from './rpc/register.js'
|
|
import { isPeerRevoked, loadPeerPolicy } from './core/peer-policy.js'
|
|
import { isInsecureOpenAdmin } from '../shared/crypto-auth.js'
|
|
import { APP_NAME, APP_VERSION } from '../shared/protocol.js'
|
|
import { openDb, closeDb, getDb } from './db/index.js'
|
|
import { startReplication, stopReplication } from './db/replicate.js'
|
|
import { startPipeline } from './pipeline.js'
|
|
import { startRestServer } from './rest/http-server.js'
|
|
import { getCollector } from './services/collector.js'
|
|
import { flushWarmPending } from './services/warm-flush.js'
|
|
import { loadRetentionConfig, startAutoPrune, stopAutoPrune } from './services/retention.js'
|
|
import logger from './utils/logger.js'
|
|
|
|
const bootStarted = Date.now()
|
|
const log = logger.child('server')
|
|
|
|
/** @type {{ keyPair: any, publicKeyHex: string, seedHex: string }} */
|
|
let keyPair
|
|
let publicKeyHex
|
|
let seedHex
|
|
try {
|
|
;({ keyPair, publicKeyHex, seedHex } = loadOrCreateKeyPair())
|
|
initAuthKeys({ seedHex, publicKeyHex })
|
|
loadPeerPolicy()
|
|
} catch (err) {
|
|
log.error('Failed to load server keypair', { error: err.message })
|
|
process.exit(1)
|
|
}
|
|
|
|
if (isInsecureOpenAdmin()) {
|
|
log.warn(
|
|
'PEARDATA_INSECURE_OPEN_ADMIN=1 — every peer is admin. Disable for production multi-operator use.'
|
|
)
|
|
}
|
|
|
|
const db = await openDb()
|
|
if (db) {
|
|
try {
|
|
await db.putNode({
|
|
nodeId: publicKeyHex.slice(0, 16),
|
|
hostname: os.hostname(),
|
|
publicKeyHex,
|
|
platform: os.platform(),
|
|
arch: os.arch(),
|
|
cpus: os.cpus().length,
|
|
totalMemMiB: Math.floor(os.totalmem() / (1024 * 1024)),
|
|
agentVersion: APP_VERSION,
|
|
updatedAt: Date.now(),
|
|
})
|
|
} catch (err) {
|
|
log.warn('Failed to upsert local node record', { error: err.message })
|
|
}
|
|
}
|
|
|
|
await startReplication()
|
|
loadRetentionConfig()
|
|
startPipeline()
|
|
startAutoPrune()
|
|
const restServer = startRestServer()
|
|
|
|
let restTunnelInfo = null
|
|
try {
|
|
const { startRestTunnel } = await import('./services/rest-tunnel.js')
|
|
restTunnelInfo = await startRestTunnel()
|
|
} catch (err) {
|
|
log.warn('REST tunnel failed to start', { error: err.message })
|
|
}
|
|
|
|
const dht = new DHT()
|
|
const server = dht.createServer()
|
|
|
|
server.on('connection', (socket) => {
|
|
const peerId = socket.remotePublicKey
|
|
? b4a.toString(socket.remotePublicKey, 'hex')
|
|
: null
|
|
|
|
if (peerId && isPeerRevoked(peerId)) {
|
|
log.warn('Rejected revoked peer', { peerId: peerId.slice(0, 12) })
|
|
try {
|
|
socket.destroy()
|
|
} catch {
|
|
// ignore
|
|
}
|
|
return
|
|
}
|
|
|
|
const session = new PeerSession(socket, {
|
|
serverPublicKey: keyPair.publicKey,
|
|
onClose: (s) => {
|
|
cleanupSession(s)
|
|
peers.remove(s.id)
|
|
log.info('Peer disconnected', {
|
|
peerId: s.id.slice(0, 12),
|
|
role: s.role,
|
|
})
|
|
},
|
|
})
|
|
|
|
registerAllHandlers(session)
|
|
peers.add(session)
|
|
|
|
log.info('Peer connected', {
|
|
peerId: session.id.slice(0, 12),
|
|
role: session.role,
|
|
connected: peers.size(),
|
|
})
|
|
})
|
|
|
|
await server.listen(keyPair)
|
|
|
|
const bootMs = Date.now() - bootStarted
|
|
const restPort = Number(process.env.PEARDATA_REST_PORT) || 18888
|
|
const restHost = process.env.PEARDATA_REST_HOST || '127.0.0.1'
|
|
const model = getDb()
|
|
|
|
logger.banner({
|
|
title: `${APP_NAME} agent v${APP_VERSION}`,
|
|
publicKey: publicKeyHex,
|
|
connect: `Client → dial ${publicKeyHex}`,
|
|
admin: 'Use SERVER_SEED as admin proof, or mint pd1 invites',
|
|
rest: restServer ? `http://${restHost}:${restPort}/api/v3/info` : 'disabled',
|
|
restTunnel: restTunnelInfo?.publicKeyHex
|
|
? `HyperDHT ${restTunnelInfo.publicKeyHex}`
|
|
: 'off',
|
|
hyperdb: model ? model.publicKeyHex : 'disabled',
|
|
swarm: process.env.PEARDATA_SWARM === '1' ? 'on' : 'off',
|
|
insecure: isInsecureOpenAdmin() ? 'OPEN ADMIN (dev)' : 'secure defaults',
|
|
bootMs: `${bootMs}ms`,
|
|
})
|
|
|
|
log.info('HyperDHT agent listening', { publicKeyHex })
|
|
|
|
async function shutdown() {
|
|
log.info('Shutting down…')
|
|
try {
|
|
getCollector().stop()
|
|
} catch {
|
|
// ignore
|
|
}
|
|
try {
|
|
const { getDockerCollector, isDockerCollectorEnabled } = await import(
|
|
'./services/collectors/docker.js'
|
|
)
|
|
if (isDockerCollectorEnabled()) getDockerCollector().stop()
|
|
} catch {
|
|
// ignore
|
|
}
|
|
try {
|
|
const { getProcessCollector, isProcessCollectorEnabled } = await import(
|
|
'./services/collectors/processes.js'
|
|
)
|
|
if (isProcessCollectorEnabled()) getProcessCollector().stop()
|
|
} catch {
|
|
// ignore
|
|
}
|
|
try {
|
|
const { getParentCollector, isParentEnabled } = await import(
|
|
'./services/collectors/parent.js'
|
|
)
|
|
if (isParentEnabled()) getParentCollector().stop()
|
|
} catch {
|
|
// ignore
|
|
}
|
|
try {
|
|
const { getNginxCollector, isNginxEnabled } = await import(
|
|
'./services/collectors/nginx.js'
|
|
)
|
|
if (isNginxEnabled()) getNginxCollector().stop()
|
|
} catch {
|
|
// ignore
|
|
}
|
|
try {
|
|
const { getRedisCollector, isRedisEnabled } = await import(
|
|
'./services/collectors/redis.js'
|
|
)
|
|
if (isRedisEnabled()) getRedisCollector().stop()
|
|
} catch {
|
|
// ignore
|
|
}
|
|
try {
|
|
const { getPostgresCollector, isPostgresEnabled } = await import(
|
|
'./services/collectors/postgres.js'
|
|
)
|
|
if (isPostgresEnabled()) getPostgresCollector().stop()
|
|
} catch {
|
|
// ignore
|
|
}
|
|
try {
|
|
const { getPearDockCollector, isPearDockEnabled } = await import(
|
|
'./services/collectors/peardock.js'
|
|
)
|
|
if (isPearDockEnabled()) getPearDockCollector().stop()
|
|
} catch {
|
|
// ignore
|
|
}
|
|
try {
|
|
stopAutoPrune()
|
|
} catch {
|
|
// ignore
|
|
}
|
|
try {
|
|
await flushWarmPending()
|
|
} catch {
|
|
// ignore
|
|
}
|
|
for (const s of peers.list()) {
|
|
try {
|
|
s.destroy()
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
if (restServer) {
|
|
await new Promise((resolve) => restServer.close(() => resolve()))
|
|
}
|
|
try {
|
|
const { stopRestTunnel } = await import('./services/rest-tunnel.js')
|
|
await stopRestTunnel()
|
|
} catch {
|
|
// ignore
|
|
}
|
|
try {
|
|
await server.close()
|
|
} catch {
|
|
// ignore
|
|
}
|
|
try {
|
|
await dht.destroy()
|
|
} catch {
|
|
// ignore
|
|
}
|
|
await stopReplication()
|
|
await closeDb()
|
|
process.exit(0)
|
|
}
|
|
|
|
gracefulGoodbye(shutdown)
|
|
process.on('SIGINT', shutdown)
|
|
process.on('SIGTERM', shutdown)
|
|
|
|
export { publicKeyHex, dht, server, restServer }
|