|
|
|
@@ -19,14 +19,6 @@ 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').
|
|
|
|
@@ -163,86 +155,76 @@ export async function checkStaleness(): Promise<number> {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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.
|
|
|
|
|
* Collect EasyPanel server metrics + container stats via SSH.
|
|
|
|
|
* A API tRPC do EasyPanel não expõe endpoint monitor.* nesta versão.
|
|
|
|
|
* SSH com password ao Easy server (5.9.90.70) funciona a partir do container.
|
|
|
|
|
*/
|
|
|
|
|
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
|
|
|
|
|
const { collectSSHMetrics } = await import('./server-metrics.js')
|
|
|
|
|
const result = await collectSSHMetrics()
|
|
|
|
|
return result.success > 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Collect Docker container/task stats via EasyPanel API.
|
|
|
|
|
* Updates the 'container' category in monitoring DB.
|
|
|
|
|
* Collect Docker Swarm service status via SSH to EasyPanel server.
|
|
|
|
|
* Usa `docker service ls` para obter replicas actual vs desired.
|
|
|
|
|
*/
|
|
|
|
|
export async function collectEasyPanelContainers(): Promise<boolean> {
|
|
|
|
|
const tasks = await callEasyPanelAPI('monitor.getDockerTaskStats')
|
|
|
|
|
if (!tasks) return false
|
|
|
|
|
const easyHost = process.env.EASY_HOST || '5.9.90.70'
|
|
|
|
|
const easyUser = process.env.EASY_USER || 'root'
|
|
|
|
|
const easyPass = process.env.EASY_PASS || ''
|
|
|
|
|
|
|
|
|
|
let total = 0, up = 0, down = 0
|
|
|
|
|
const unhealthy: string[] = []
|
|
|
|
|
if (!easyPass) return false
|
|
|
|
|
|
|
|
|
|
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_', ''))
|
|
|
|
|
try {
|
|
|
|
|
const { Client } = await import('ssh2')
|
|
|
|
|
const output = await new Promise<string>((resolve, reject) => {
|
|
|
|
|
const conn = new Client()
|
|
|
|
|
let data = ''
|
|
|
|
|
const timer = setTimeout(() => { conn.end(); reject(new Error('timeout')) }, 20000)
|
|
|
|
|
|
|
|
|
|
conn.on('ready', () => {
|
|
|
|
|
conn.exec("docker service ls --format '{{.Name}} {{.Replicas}}'", (err, stream) => {
|
|
|
|
|
if (err) { clearTimeout(timer); conn.end(); reject(err); return }
|
|
|
|
|
stream.on('data', (chunk: Buffer) => { data += chunk.toString() })
|
|
|
|
|
stream.on('close', () => { clearTimeout(timer); conn.end(); resolve(data) })
|
|
|
|
|
stream.stderr.on('data', () => {})
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
conn.on('error', (err) => { clearTimeout(timer); reject(err) })
|
|
|
|
|
conn.connect({ host: easyHost, port: 22, username: easyUser, password: easyPass, readyTimeout: 15000 })
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
let total = 0, up = 0, down = 0
|
|
|
|
|
const unhealthy: string[] = []
|
|
|
|
|
|
|
|
|
|
for (const line of output.trim().split('\n')) {
|
|
|
|
|
if (!line.trim()) continue
|
|
|
|
|
const parts = line.trim().split(/\s+/)
|
|
|
|
|
const name = parts[0] || ''
|
|
|
|
|
const replicas = parts[1] || '0/0'
|
|
|
|
|
const [actual, desired] = replicas.split('/').map(Number)
|
|
|
|
|
total++
|
|
|
|
|
if (actual >= desired && desired > 0) {
|
|
|
|
|
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
|
|
|
|
|
} catch (err: unknown) {
|
|
|
|
|
console.error('[EASYPANEL] Container collection failed:', err instanceof Error ? err.message : err)
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@@ -264,7 +246,7 @@ export async function collectMonitoringData(): Promise<void> {
|
|
|
|
|
const gotStats = await collectEasyPanelMetrics()
|
|
|
|
|
const gotContainers = await collectEasyPanelContainers()
|
|
|
|
|
if (!gotStats && !gotContainers) {
|
|
|
|
|
console.warn('[COLLECTOR] EasyPanel API unavailable (check EASYPANEL_API_TOKEN)')
|
|
|
|
|
console.warn('[COLLECTOR] EasyPanel metrics unavailable (check EASY_HOST/EASY_USER/EASY_PASS)')
|
|
|
|
|
}
|
|
|
|
|
} catch (err: unknown) {
|
|
|
|
|
console.error('[COLLECTOR] EasyPanel collection failed:', err instanceof Error ? err.message : err)
|
|
|
|
|