feat: replace SSH with EasyPanel API for Easy server metrics
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Monitoring Data Collector
|
||||
* HTTP health checks for services + staleness detection for WP sites
|
||||
* HTTP health checks for services + EasyPanel API metrics + staleness detection
|
||||
* Runs every 5 minutes via scheduler in server.ts
|
||||
* @author Descomplicar® | @link descomplicar.pt | @copyright 2026
|
||||
*/
|
||||
@@ -19,6 +19,14 @@ interface CheckResult {
|
||||
error?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* EasyPanel API config.
|
||||
* Accessible from Docker Swarm via service name 'easypanel'.
|
||||
* Token read from EASYPANEL_API_TOKEN env var.
|
||||
*/
|
||||
const EASYPANEL_API_URL = process.env.EASYPANEL_API_URL || 'http://easypanel:3000/api/trpc'
|
||||
const EASYPANEL_API_TOKEN = process.env.EASYPANEL_API_TOKEN || ''
|
||||
|
||||
/**
|
||||
* Services to monitor via HTTP health check.
|
||||
* Each entry maps to a record in tbl_eal_monitoring (category='service').
|
||||
@@ -33,7 +41,7 @@ const SERVICES: ServiceCheck[] = [
|
||||
{ name: 'Metabase', url: 'https://bi.descomplicar.pt' },
|
||||
{ name: 'N8N', url: 'https://automator.descomplicar.pt' },
|
||||
{ name: 'Outline', url: 'https://hub.descomplicar.pt' },
|
||||
{ name: 'WhatSMS', url: 'https://whatsms.pt' },
|
||||
{ name: 'WhatSMS', url: 'https://app.whatsms.pt' },
|
||||
{ name: 'MCP Gateway', url: 'http://gateway.descomplicar.pt', okStatuses: [403] },
|
||||
]
|
||||
|
||||
@@ -154,6 +162,89 @@ export async function checkStaleness(): Promise<number> {
|
||||
return (result as any).affectedRows || 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Call EasyPanel tRPC API endpoint.
|
||||
* Returns parsed JSON or null on failure.
|
||||
*/
|
||||
async function callEasyPanelAPI(endpoint: string): Promise<any | null> {
|
||||
if (!EASYPANEL_API_TOKEN) return null
|
||||
|
||||
try {
|
||||
const controller = new AbortController()
|
||||
const timeout = setTimeout(() => controller.abort(), 10000)
|
||||
|
||||
const response = await fetch(`${EASYPANEL_API_URL}/${endpoint}`, {
|
||||
headers: { 'Authorization': `Bearer ${EASYPANEL_API_TOKEN}` },
|
||||
signal: controller.signal,
|
||||
})
|
||||
|
||||
clearTimeout(timeout)
|
||||
if (!response.ok) return null
|
||||
|
||||
const data: any = await response.json()
|
||||
return data?.result?.data?.json ?? null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect EasyPanel server metrics (CPU, RAM, disk) via API.
|
||||
* Replaces SSH-based collection for the Easy server.
|
||||
*/
|
||||
export async function collectEasyPanelMetrics(): Promise<boolean> {
|
||||
const stats = await callEasyPanelAPI('monitor.getSystemStats')
|
||||
if (!stats) return false
|
||||
|
||||
const cpu = Math.round(stats.cpuInfo?.usedPercentage ?? 0)
|
||||
const ram = Math.round((stats.memInfo?.usedMemPercentage ?? 0) * 10) / 10
|
||||
const disk = parseFloat(stats.diskInfo?.usedPercentage ?? '0')
|
||||
const load = stats.cpuInfo?.loadavg?.[0] ?? 0
|
||||
|
||||
await upsertMonitoring('server', 'EasyPanel', 'up', {
|
||||
cpu, ram, disk, load,
|
||||
uptime_hours: Math.round((stats.uptime ?? 0) / 3600),
|
||||
mem_total_mb: Math.round(stats.memInfo?.totalMemMb ?? 0),
|
||||
mem_used_mb: Math.round(stats.memInfo?.usedMemMb ?? 0),
|
||||
disk_total_gb: stats.diskInfo?.totalGb,
|
||||
disk_free_gb: stats.diskInfo?.freeGb,
|
||||
})
|
||||
|
||||
console.log(`[EASYPANEL] Server: CPU=${cpu}%, RAM=${ram}%, Disk=${disk}%`)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect Docker container/task stats via EasyPanel API.
|
||||
* Updates the 'container' category in monitoring DB.
|
||||
*/
|
||||
export async function collectEasyPanelContainers(): Promise<boolean> {
|
||||
const tasks = await callEasyPanelAPI('monitor.getDockerTaskStats')
|
||||
if (!tasks) return false
|
||||
|
||||
let total = 0, up = 0, down = 0
|
||||
const unhealthy: string[] = []
|
||||
|
||||
for (const [name, info] of Object.entries(tasks) as [string, { actual: number; desired: number }][]) {
|
||||
total++
|
||||
if (info.actual >= info.desired) {
|
||||
up++
|
||||
} else {
|
||||
down++
|
||||
unhealthy.push(name.replace('descomplicar_', ''))
|
||||
}
|
||||
}
|
||||
|
||||
const status = down > 0 ? 'warning' : 'ok'
|
||||
await upsertMonitoring('container', 'EasyPanel Containers', status, {
|
||||
total, up, down, restarting: 0,
|
||||
...(unhealthy.length > 0 ? { unhealthy } : {}),
|
||||
})
|
||||
|
||||
console.log(`[EASYPANEL] Containers: ${up}/${total} running${down > 0 ? `, ${down} down: ${unhealthy.join(', ')}` : ''}`)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Main collector entry point.
|
||||
* Called by scheduler in server.ts every 5 minutes.
|
||||
@@ -168,6 +259,17 @@ export async function collectMonitoringData(): Promise<void> {
|
||||
console.error('[COLLECTOR] Service checks failed:', err instanceof Error ? err.message : err)
|
||||
}
|
||||
|
||||
// EasyPanel API metrics (replaces SSH for Easy server)
|
||||
try {
|
||||
const gotStats = await collectEasyPanelMetrics()
|
||||
const gotContainers = await collectEasyPanelContainers()
|
||||
if (!gotStats && !gotContainers) {
|
||||
console.warn('[COLLECTOR] EasyPanel API unavailable (check EASYPANEL_API_TOKEN)')
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
console.error('[COLLECTOR] EasyPanel collection failed:', err instanceof Error ? err.message : err)
|
||||
}
|
||||
|
||||
try {
|
||||
const stale = await checkStaleness()
|
||||
if (stale > 0) {
|
||||
|
||||
Reference in New Issue
Block a user