toes/src/cli/commands/metrics.ts

116 lines
3.9 KiB
TypeScript

import color from 'ansis'
import { get } from '../http'
import { resolveAppName } from '../name'
interface AppMetrics {
name: string
state: string
port?: number
pid?: number
cpu?: number
memory?: number
rss?: number
dataSize?: number
tool?: boolean
}
function formatBytes(bytes?: number): string {
if (bytes === undefined) return '-'
if (bytes < 1024) return `${bytes} B`
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`
return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`
}
function formatRss(kb?: number): string {
if (kb === undefined) return '-'
if (kb < 1024) return `${kb} KB`
if (kb < 1024 * 1024) return `${(kb / 1024).toFixed(1)} MB`
return `${(kb / 1024 / 1024).toFixed(2)} GB`
}
function formatPercent(value?: number): string {
if (value === undefined) return '-'
return `${value.toFixed(1)}%`
}
function pad(str: string, len: number, right = false): string {
if (right) return str.padStart(len)
return str.padEnd(len)
}
export async function metricsApp(arg?: string) {
// If arg is provided, show metrics for that app only
if (arg) {
const name = resolveAppName(arg)
if (!name) return
const metrics: AppMetrics | undefined = await get(`/api/tools/metrics/api/metrics/${name}`)
if (!metrics) {
console.error(`App not found: ${name}`)
return
}
console.log(`${color.bold(metrics.name)} ${metrics.tool ? color.gray('[tool]') : ''}`)
console.log(` State: ${metrics.state}`)
if (metrics.pid) console.log(` PID: ${metrics.pid}`)
if (metrics.port) console.log(` Port: ${metrics.port}`)
if (metrics.cpu !== undefined) console.log(` CPU: ${formatPercent(metrics.cpu)}`)
if (metrics.memory !== undefined) console.log(` Memory: ${formatPercent(metrics.memory)}`)
if (metrics.rss !== undefined) console.log(` RSS: ${formatRss(metrics.rss)}`)
console.log(` Data: ${formatBytes(metrics.dataSize)}`)
return
}
// Show metrics for all apps
const metrics: AppMetrics[] | undefined = await get('/api/tools/metrics/api/metrics')
if (!metrics || metrics.length === 0) {
console.log('No apps found')
return
}
// Sort: running first, then by name
metrics.sort((a, b) => {
if (a.state === 'running' && b.state !== 'running') return -1
if (a.state !== 'running' && b.state === 'running') return 1
return a.name.localeCompare(b.name)
})
// Calculate column widths
const nameWidth = Math.max(4, ...metrics.map(s => s.name.length + (s.tool ? 7 : 0)))
const stateWidth = Math.max(5, ...metrics.map(s => s.state.length))
// Header
console.log(
color.gray(
`${pad('NAME', nameWidth)} ${pad('STATE', stateWidth)} ${pad('PID', 7, true)} ${pad('CPU', 7, true)} ${pad('MEM', 7, true)} ${pad('RSS', 10, true)} ${pad('DATA', 10, true)}`
)
)
// Rows
for (const s of metrics) {
const name = s.tool ? `${s.name} ${color.gray('[tool]')}` : s.name
const stateColor = s.state === 'running' ? color.green : s.state === 'invalid' ? color.red : color.gray
const state = stateColor(s.state)
const pid = s.pid ? String(s.pid) : '-'
const cpu = formatPercent(s.cpu)
const mem = formatPercent(s.memory)
const rss = formatRss(s.rss)
const data = formatBytes(s.dataSize)
console.log(
`${pad(name, nameWidth)} ${pad(state, stateWidth)} ${pad(pid, 7, true)} ${pad(cpu, 7, true)} ${pad(mem, 7, true)} ${pad(rss, 10, true)} ${pad(data, 10, true)}`
)
}
// Summary
const running = metrics.filter(s => s.state === 'running')
const totalCpu = running.reduce((sum, s) => sum + (s.cpu ?? 0), 0)
const totalRss = running.reduce((sum, s) => sum + (s.rss ?? 0), 0)
const totalData = metrics.reduce((sum, s) => sum + (s.dataSize ?? 0), 0)
console.log()
console.log(color.gray(`${running.length} running, ${formatPercent(totalCpu)} CPU, ${formatRss(totalRss)} RSS, ${formatBytes(totalData)} data`))
}