forked from defunkt/toes
Rename stats to metrics and add data size metric
Rename the "stats" CLI command, tool app, and all internal references to "metrics". Add file size tracking from each app's DATA_DIR as a new metric, shown in both the CLI table and web UI. https://claude.ai/code/session_013agP8J1cCfrWZkueZ33jQB
This commit is contained in:
parent
9c128eaddc
commit
eb8ef0dd4d
|
|
@ -79,7 +79,7 @@ Client-side SPA rendered with `hono/jsx/dom`. No build step -- Bun serves `.tsx`
|
|||
|
||||
CLI commands:
|
||||
- **Apps**: `list`, `info`, `new`, `get`, `open`, `rename`, `rm`
|
||||
- **Lifecycle**: `start`, `stop`, `restart`, `logs`, `stats`, `cron`
|
||||
- **Lifecycle**: `start`, `stop`, `restart`, `logs`, `metrics`, `cron`
|
||||
- **Sync**: `push`, `pull`, `status`, `diff`, `sync`, `clean`, `stash`
|
||||
- **Config**: `config`, `env`, `versions`, `history`, `rollback`
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,18 @@
|
|||
import { Hype } from '@because/hype'
|
||||
import { define, stylesToCSS } from '@because/forge'
|
||||
import { baseStyles, ToolScript, theme } from '@because/toes/tools'
|
||||
import { existsSync, readdirSync, statSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
import type { Child } from 'hono/jsx'
|
||||
|
||||
// ============================================================================
|
||||
// Configuration
|
||||
// ============================================================================
|
||||
|
||||
const SAMPLE_INTERVAL_MS = 10_000 // How often to sample process stats
|
||||
const SAMPLE_INTERVAL_MS = 10_000 // How often to sample process metrics
|
||||
const HISTORY_MAX_SAMPLES = 60 // Keep 10 minutes of history at 10s intervals
|
||||
|
||||
const TOES_DIR = process.env.TOES_DIR!
|
||||
const TOES_URL = process.env.TOES_URL!
|
||||
|
||||
// ============================================================================
|
||||
|
|
@ -24,6 +27,13 @@ interface App {
|
|||
tool?: boolean
|
||||
}
|
||||
|
||||
interface AppMetrics extends App {
|
||||
cpu?: number
|
||||
dataSize?: number
|
||||
memory?: number
|
||||
rss?: number
|
||||
}
|
||||
|
||||
interface HistorySample {
|
||||
timestamp: number
|
||||
cpu: number
|
||||
|
|
@ -31,58 +41,26 @@ interface HistorySample {
|
|||
rss: number
|
||||
}
|
||||
|
||||
interface ProcessStats {
|
||||
interface ProcessMetrics {
|
||||
pid: number
|
||||
cpu: number
|
||||
memory: number
|
||||
rss: number
|
||||
}
|
||||
|
||||
interface AppStats extends App {
|
||||
cpu?: number
|
||||
memory?: number
|
||||
rss?: number
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Process Stats Collection
|
||||
// Process Metrics Collection
|
||||
// ============================================================================
|
||||
|
||||
const statsCache = new Map<number, ProcessStats>()
|
||||
const appHistory = new Map<string, HistorySample[]>() // app name -> history
|
||||
const metricsCache = new Map<number, ProcessMetrics>()
|
||||
|
||||
async function sampleProcessStats(): Promise<void> {
|
||||
try {
|
||||
const proc = Bun.spawn(['ps', '-eo', 'pid,pcpu,pmem,rss'], {
|
||||
stdout: 'pipe',
|
||||
stderr: 'ignore',
|
||||
})
|
||||
|
||||
const text = await new Response(proc.stdout).text()
|
||||
const lines = text.trim().split('\n').slice(1) // Skip header
|
||||
|
||||
statsCache.clear()
|
||||
|
||||
for (const line of lines) {
|
||||
const parts = line.trim().split(/\s+/)
|
||||
if (parts.length >= 4) {
|
||||
const pid = parseInt(parts[0]!, 10)
|
||||
const cpu = parseFloat(parts[1]!)
|
||||
const memory = parseFloat(parts[2]!)
|
||||
const rss = parseInt(parts[3]!, 10) // KB
|
||||
|
||||
if (!isNaN(pid) && pid > 0) {
|
||||
statsCache.set(pid, { pid, cpu, memory, rss })
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to sample process stats:', err)
|
||||
}
|
||||
function getHistory(appName: string): HistorySample[] {
|
||||
return appHistory.get(appName) ?? []
|
||||
}
|
||||
|
||||
function getProcessStats(pid: number): ProcessStats | undefined {
|
||||
return statsCache.get(pid)
|
||||
function getProcessMetrics(pid: number): ProcessMetrics | undefined {
|
||||
return metricsCache.get(pid)
|
||||
}
|
||||
|
||||
function recordHistory(appName: string, cpu: number, memory: number, rss: number): void {
|
||||
|
|
@ -102,12 +80,38 @@ function recordHistory(appName: string, cpu: number, memory: number, rss: number
|
|||
appHistory.set(appName, history)
|
||||
}
|
||||
|
||||
function getHistory(appName: string): HistorySample[] {
|
||||
return appHistory.get(appName) ?? []
|
||||
async function sampleProcessMetrics(): Promise<void> {
|
||||
try {
|
||||
const proc = Bun.spawn(['ps', '-eo', 'pid,pcpu,pmem,rss'], {
|
||||
stdout: 'pipe',
|
||||
stderr: 'ignore',
|
||||
})
|
||||
|
||||
const text = await new Response(proc.stdout).text()
|
||||
const lines = text.trim().split('\n').slice(1) // Skip header
|
||||
|
||||
metricsCache.clear()
|
||||
|
||||
for (const line of lines) {
|
||||
const parts = line.trim().split(/\s+/)
|
||||
if (parts.length >= 4) {
|
||||
const pid = parseInt(parts[0]!, 10)
|
||||
const cpu = parseFloat(parts[1]!)
|
||||
const memory = parseFloat(parts[2]!)
|
||||
const rss = parseInt(parts[3]!, 10) // KB
|
||||
|
||||
if (!isNaN(pid) && pid > 0) {
|
||||
metricsCache.set(pid, { pid, cpu, memory, rss })
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to sample process metrics:', err)
|
||||
}
|
||||
}
|
||||
|
||||
async function sampleAndRecordHistory(): Promise<void> {
|
||||
await sampleProcessStats()
|
||||
await sampleProcessMetrics()
|
||||
|
||||
// Record history for all running apps
|
||||
try {
|
||||
|
|
@ -117,9 +121,9 @@ async function sampleAndRecordHistory(): Promise<void> {
|
|||
|
||||
for (const app of apps) {
|
||||
if (app.pid && app.state === 'running') {
|
||||
const stats = getProcessStats(app.pid)
|
||||
if (stats) {
|
||||
recordHistory(app.name, stats.cpu, stats.memory, stats.rss)
|
||||
const metrics = getProcessMetrics(app.pid)
|
||||
if (metrics) {
|
||||
recordHistory(app.name, metrics.cpu, metrics.memory, metrics.rss)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -132,6 +136,29 @@ async function sampleAndRecordHistory(): Promise<void> {
|
|||
sampleAndRecordHistory()
|
||||
setInterval(sampleAndRecordHistory, SAMPLE_INTERVAL_MS)
|
||||
|
||||
// ============================================================================
|
||||
// Data Size
|
||||
// ============================================================================
|
||||
|
||||
function getDataSize(appName: string): number {
|
||||
const dataDir = join(TOES_DIR, appName)
|
||||
if (!existsSync(dataDir)) return 0
|
||||
return dirSize(dataDir)
|
||||
}
|
||||
|
||||
function dirSize(dir: string): number {
|
||||
let total = 0
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
const path = join(dir, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
total += dirSize(path)
|
||||
} else {
|
||||
total += statSync(path).size
|
||||
}
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// API Client
|
||||
// ============================================================================
|
||||
|
|
@ -146,30 +173,32 @@ async function fetchApps(): Promise<App[]> {
|
|||
}
|
||||
}
|
||||
|
||||
async function getAppStats(): Promise<AppStats[]> {
|
||||
async function getAppMetrics(): Promise<AppMetrics[]> {
|
||||
const apps = await fetchApps()
|
||||
return apps.map(app => {
|
||||
const stats = app.pid ? getProcessStats(app.pid) : undefined
|
||||
const metrics = app.pid ? getProcessMetrics(app.pid) : undefined
|
||||
return {
|
||||
...app,
|
||||
cpu: stats?.cpu,
|
||||
memory: stats?.memory,
|
||||
rss: stats?.rss,
|
||||
cpu: metrics?.cpu,
|
||||
dataSize: getDataSize(app.name),
|
||||
memory: metrics?.memory,
|
||||
rss: metrics?.rss,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function getAppStatsByName(name: string): Promise<AppStats | undefined> {
|
||||
async function getAppMetricsByName(name: string): Promise<AppMetrics | undefined> {
|
||||
const apps = await fetchApps()
|
||||
const app = apps.find(a => a.name === name)
|
||||
if (!app) return undefined
|
||||
|
||||
const stats = app.pid ? getProcessStats(app.pid) : undefined
|
||||
const metrics = app.pid ? getProcessMetrics(app.pid) : undefined
|
||||
return {
|
||||
...app,
|
||||
cpu: stats?.cpu,
|
||||
memory: stats?.memory,
|
||||
rss: stats?.rss,
|
||||
cpu: metrics?.cpu,
|
||||
dataSize: getDataSize(app.name),
|
||||
memory: metrics?.memory,
|
||||
rss: metrics?.rss,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -310,11 +339,12 @@ const NoDataMessage = define('NoDataMessage', {
|
|||
// Helpers
|
||||
// ============================================================================
|
||||
|
||||
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 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 formatPercent(value?: number): string {
|
||||
|
|
@ -322,6 +352,13 @@ function formatPercent(value?: number): string {
|
|||
return `${value.toFixed(1)}%`
|
||||
}
|
||||
|
||||
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 getStatusColor(state: string): string {
|
||||
switch (state) {
|
||||
case 'running':
|
||||
|
|
@ -377,18 +414,18 @@ app.get('/styles.css', c => c.text(baseStyles + stylesToCSS(), 200, {
|
|||
}))
|
||||
|
||||
// API endpoint for CLI
|
||||
app.get('/api/stats', async c => {
|
||||
const stats = await getAppStats()
|
||||
return c.json(stats)
|
||||
app.get('/api/metrics', async c => {
|
||||
const metrics = await getAppMetrics()
|
||||
return c.json(metrics)
|
||||
})
|
||||
|
||||
app.get('/api/stats/:name', async c => {
|
||||
app.get('/api/metrics/:name', async c => {
|
||||
const name = c.req.param('name')
|
||||
const stats = await getAppStatsByName(name)
|
||||
if (!stats) {
|
||||
const metrics = await getAppMetricsByName(name)
|
||||
if (!metrics) {
|
||||
return c.json({ error: 'App not found' }, 404)
|
||||
}
|
||||
return c.json(stats)
|
||||
return c.json(metrics)
|
||||
})
|
||||
|
||||
app.get('/api/history/:name', c => {
|
||||
|
|
@ -403,18 +440,18 @@ app.get('/', async c => {
|
|||
|
||||
// Single app view
|
||||
if (appName) {
|
||||
const stats = await getAppStatsByName(appName)
|
||||
const metrics = await getAppMetricsByName(appName)
|
||||
|
||||
if (!stats) {
|
||||
if (!metrics) {
|
||||
return c.html(
|
||||
<Layout title="Stats">
|
||||
<Layout title="Metrics">
|
||||
<EmptyState>App not found: {appName}</EmptyState>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
return c.html(
|
||||
<Layout title="Stats">
|
||||
<Layout title="Metrics">
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -423,19 +460,21 @@ app.get('/', async c => {
|
|||
<ThRight>CPU</ThRight>
|
||||
<ThRight>MEM</ThRight>
|
||||
<ThRight>RSS</ThRight>
|
||||
<ThRight>Data</ThRight>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<Tr>
|
||||
<Td>
|
||||
<StatusBadge style={`color: ${getStatusColor(stats.state)}`}>
|
||||
{stats.state}
|
||||
<StatusBadge style={`color: ${getStatusColor(metrics.state)}`}>
|
||||
{metrics.state}
|
||||
</StatusBadge>
|
||||
</Td>
|
||||
<TdRight>{stats.pid ?? '-'}</TdRight>
|
||||
<TdRight>{formatPercent(stats.cpu)}</TdRight>
|
||||
<TdRight>{formatPercent(stats.memory)}</TdRight>
|
||||
<TdRight>{formatRss(stats.rss)}</TdRight>
|
||||
<TdRight>{metrics.pid ?? '-'}</TdRight>
|
||||
<TdRight>{formatPercent(metrics.cpu)}</TdRight>
|
||||
<TdRight>{formatPercent(metrics.memory)}</TdRight>
|
||||
<TdRight>{formatRss(metrics.rss)}</TdRight>
|
||||
<TdRight>{formatBytes(metrics.dataSize)}</TdRight>
|
||||
</Tr>
|
||||
</tbody>
|
||||
</Table>
|
||||
|
|
@ -674,29 +713,30 @@ app.get('/', async c => {
|
|||
}
|
||||
|
||||
// All apps view
|
||||
const stats = await getAppStats()
|
||||
const metrics = await getAppMetrics()
|
||||
|
||||
// Sort: running first, then by name
|
||||
stats.sort((a, b) => {
|
||||
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)
|
||||
})
|
||||
|
||||
if (stats.length === 0) {
|
||||
if (metrics.length === 0) {
|
||||
return c.html(
|
||||
<Layout title="Stats">
|
||||
<Layout title="Metrics">
|
||||
<EmptyState>No apps found</EmptyState>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
const running = stats.filter(s => s.state === 'running')
|
||||
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)
|
||||
|
||||
return c.html(
|
||||
<Layout title="Stats">
|
||||
<Layout title="Metrics">
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -706,10 +746,11 @@ app.get('/', async c => {
|
|||
<ThRight>CPU</ThRight>
|
||||
<ThRight>MEM</ThRight>
|
||||
<ThRight>RSS</ThRight>
|
||||
<ThRight>Data</ThRight>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{stats.map(s => (
|
||||
{metrics.map(s => (
|
||||
<Tr>
|
||||
<Td>
|
||||
{s.name}
|
||||
|
|
@ -724,12 +765,13 @@ app.get('/', async c => {
|
|||
<TdRight>{formatPercent(s.cpu)}</TdRight>
|
||||
<TdRight>{formatPercent(s.memory)}</TdRight>
|
||||
<TdRight>{formatRss(s.rss)}</TdRight>
|
||||
<TdRight>{formatBytes(s.dataSize)}</TdRight>
|
||||
</Tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
<Summary>
|
||||
{running.length} running · {formatPercent(totalCpu)} CPU · {formatRss(totalRss)} total
|
||||
{running.length} running · {formatPercent(totalCpu)} CPU · {formatRss(totalRss)} RSS · {formatBytes(totalData)} data
|
||||
</Summary>
|
||||
</Layout>
|
||||
)
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "stats",
|
||||
"name": "metrics",
|
||||
"module": "index.tsx",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
|
|
@ -13,5 +13,5 @@ export {
|
|||
startApp,
|
||||
stopApp,
|
||||
} from './manage'
|
||||
export { statsApp } from './stats'
|
||||
export { metricsApp } from './metrics'
|
||||
export { cleanApp, diffApp, getApp, historyApp, pullApp, pushApp, rollbackApp, stashApp, stashListApp, stashPopApp, statusApp, syncApp, versionsApp } from './sync'
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import color from 'kleur'
|
|||
import { get } from '../http'
|
||||
import { resolveAppName } from '../name'
|
||||
|
||||
interface AppStats {
|
||||
interface AppMetrics {
|
||||
name: string
|
||||
state: string
|
||||
port?: number
|
||||
|
|
@ -10,9 +10,18 @@ interface AppStats {
|
|||
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`
|
||||
|
|
@ -30,55 +39,56 @@ function pad(str: string, len: number, right = false): string {
|
|||
return str.padEnd(len)
|
||||
}
|
||||
|
||||
export async function statsApp(arg?: string) {
|
||||
// If arg is provided, show stats for that app only
|
||||
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 stats: AppStats | undefined = await get(`/api/tools/stats/api/stats/${name}`)
|
||||
if (!stats) {
|
||||
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(stats.name)} ${stats.tool ? color.gray('[tool]') : ''}`)
|
||||
console.log(` State: ${stats.state}`)
|
||||
if (stats.pid) console.log(` PID: ${stats.pid}`)
|
||||
if (stats.port) console.log(` Port: ${stats.port}`)
|
||||
if (stats.cpu !== undefined) console.log(` CPU: ${formatPercent(stats.cpu)}`)
|
||||
if (stats.memory !== undefined) console.log(` Memory: ${formatPercent(stats.memory)}`)
|
||||
if (stats.rss !== undefined) console.log(` RSS: ${formatRss(stats.rss)}`)
|
||||
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 stats for all apps
|
||||
const stats: AppStats[] | undefined = await get('/api/tools/stats/api/stats')
|
||||
if (!stats || stats.length === 0) {
|
||||
// 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
|
||||
stats.sort((a, b) => {
|
||||
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, ...stats.map(s => s.name.length + (s.tool ? 7 : 0)))
|
||||
const stateWidth = Math.max(5, ...stats.map(s => s.state.length))
|
||||
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('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 stats) {
|
||||
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)
|
||||
|
|
@ -87,17 +97,19 @@ export async function statsApp(arg?: string) {
|
|||
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(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 = stats.filter(s => s.state === 'running')
|
||||
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)} total`))
|
||||
console.log(color.gray(`${running.length} running, ${formatPercent(totalCpu)} CPU, ${formatRss(totalRss)} RSS, ${formatBytes(totalData)} data`))
|
||||
}
|
||||
|
|
@ -32,7 +32,7 @@ import {
|
|||
stashListApp,
|
||||
stashPopApp,
|
||||
startApp,
|
||||
statsApp,
|
||||
metricsApp,
|
||||
statusApp,
|
||||
stopApp,
|
||||
syncApp,
|
||||
|
|
@ -161,11 +161,11 @@ program
|
|||
.action(logApp)
|
||||
|
||||
program
|
||||
.command('stats')
|
||||
.command('metrics')
|
||||
.helpGroup('Lifecycle:')
|
||||
.description('Show CPU and memory stats for apps')
|
||||
.description('Show CPU, memory, and disk metrics for apps')
|
||||
.argument('[name]', 'app name (uses current directory if omitted)')
|
||||
.action(statsApp)
|
||||
.action(metricsApp)
|
||||
|
||||
const cron = program
|
||||
.command('cron')
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user