#!/usr/bin/env python3 """ App Usage Tracker — 进程 + Docker 容器资源用量排行 按日/周/月/年/总计 聚合 CPU、内存、网络流量 """ import json import os import re import sqlite3 import subprocess import time from datetime import date, datetime, timedelta DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'app_usage.db') _LAST_SNAPSHOT_TS = 0 _SNAPSHOT_INTERVAL = 60 # collect snapshots every 60s def _get_db(): conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute('''CREATE TABLE IF NOT EXISTS snapshots ( ts INTEGER, app TEXT, cpu_pct REAL, mem_mb REAL, net_rx INTEGER, net_tx INTEGER )''') c.execute('CREATE INDEX IF NOT EXISTS idx_snap_ts ON snapshots(ts)') c.execute('CREATE INDEX IF NOT EXISTS idx_snap_app ON snapshots(app)') c.execute('''CREATE TABLE IF NOT EXISTS daily_rollup ( day TEXT, app TEXT, cpu_avg REAL, mem_avg REAL, net_rx INTEGER, net_tx INTEGER, PRIMARY KEY (day, app) )''') c.execute('''CREATE TABLE IF NOT EXISTS weekly_rollup ( week TEXT, app TEXT, cpu_avg REAL, mem_avg REAL, net_rx INTEGER, net_tx INTEGER, PRIMARY KEY (week, app) )''') c.execute('''CREATE TABLE IF NOT EXISTS monthly_rollup ( month TEXT, app TEXT, cpu_avg REAL, mem_avg REAL, net_rx INTEGER, net_tx INTEGER, PRIMARY KEY (month, app) )''') c.execute('''CREATE TABLE IF NOT EXISTS yearly_rollup ( year TEXT, app TEXT, cpu_avg REAL, mem_avg REAL, net_rx INTEGER, net_tx INTEGER, PRIMARY KEY (year, app) )''') conn.commit() return conn def _get_process_list(): """Get current processes: {comm: {cpu_pct, mem_mb}}""" procs = {} try: result = os.popen("ps -eo comm,pcpu,rss --no-headers 2>/dev/null").read() for line in result.strip().split('\n'): parts = line.strip().split(None, 2) if len(parts) >= 3: name = parts[0] # Normalize names: strip docker prefixes, version suffixes if name.startswith('docker-'): name = name[7:] # strip 'docker-' name = re.sub(r'[-_]\d+$', '', name) # strip trailing numbers name = re.sub(r'[-_](v?\d[\d.]*|arm.*|amd64)$', '', name) try: cpu = float(parts[1]) except ValueError: cpu = 0 try: mem_kb = int(parts[2]) except ValueError: mem_kb = 0 if name not in procs: procs[name] = {'cpu': 0, 'mem': 0} procs[name]['cpu'] += cpu procs[name]['mem'] += mem_kb except Exception: pass # Convert mem from KB to MB for v in procs.values(): v['mem'] = round(v['mem'] / 1024, 1) return procs def _get_docker_traffic(): """Get per-container network: {name: {rx, tx}}""" traffic = {} try: result = subprocess.run( ['docker', 'stats', '--no-stream', '--format', '{{.Name}}\t{{.NetIO}}'], capture_output=True, text=True, timeout=15 ) for line in result.stdout.strip().split('\n'): parts = line.strip().split('\t') if len(parts) < 2: continue name = parts[0] netio = parts[1].strip() rx_str, tx_str = '', '' if ' / ' in netio: rx_str, tx_str = netio.split(' / ', 1) def parse(s): s = s.strip().replace(',', '') m = re.match(r'([\d.]+)\s*([a-zA-Z]+)', s) if not m: return 0 v = float(m.group(1)) u = m.group(2).lower() mul = {'b': 1, 'kb': 1000, 'kib': 1024, 'mb': 1e6, 'mib': 1048576, 'gb': 1e9, 'gib': 1073741824} return int(v * mul.get(u, 1)) traffic[name] = {'rx': parse(rx_str), 'tx': parse(tx_str)} except Exception: pass return traffic def collect_snapshot(): """Collect and store one snapshot of app usage.""" global _LAST_SNAPSHOT_TS now = int(time.time()) if now - _LAST_SNAPSHOT_TS < _SNAPSHOT_INTERVAL: return _LAST_SNAPSHOT_TS = now procs = _get_process_list() dockers = _get_docker_traffic() conn = _get_db() c = conn.cursor() # Process CPU + mem for app, data in procs.items(): net_rx = dockers.get(app, {}).get('rx', 0) net_tx = dockers.get(app, {}).get('tx', 0) c.execute( 'INSERT INTO snapshots VALUES (?,?,?,?,?,?)', (now, app, data['cpu'], data['mem'], net_rx, net_tx) ) # Docker-only apps (no matching process) for app, data in dockers.items(): if app not in procs: c.execute( 'INSERT INTO snapshots VALUES (?,?,?,?,?,?)', (now, app, 0, 0, data['rx'], data['tx']) ) conn.commit() # Cleanup old snapshots (keep last 7 days of raw data) c.execute('DELETE FROM snapshots WHERE ts < ?', (now - 7 * 86400,)) conn.commit() conn.close() # Roll up daily/weekly/monthly/yearly _rollup_all() def _rollup_all(): """Roll up snapshots into daily/weekly/monthly/yearly aggregates.""" conn = _get_db() c = conn.cursor() now = datetime.now() today = now.strftime('%Y-%m-%d') this_week = now.strftime('%G-W%V') # ISO week this_month = now.strftime('%Y-%m') this_year = now.strftime('%Y') periods = [ ('daily_rollup', 'day', today), ('weekly_rollup', 'week', this_week), ('monthly_rollup', 'month', this_month), ('yearly_rollup', 'year', this_year), ] for table, col, period_val in periods: # Aggregate from snapshots (last 24h for daily, etc.) cutoff = int(time.time()) - 86400 # last 24h for all rollups c.execute(f''' INSERT OR REPLACE INTO {table} ({col}, app, cpu_avg, mem_avg, net_rx, net_tx) SELECT ?, app, ROUND(AVG(cpu_pct), 1), ROUND(AVG(mem_mb), 1), MAX(net_rx), MAX(net_tx) FROM snapshots WHERE ts >= ? GROUP BY app ORDER BY AVG(cpu_pct) DESC ''', (period_val, cutoff)) conn.commit() conn.close() def get_app_ranking(period='total', metric='cpu', limit=15): """ Get app usage ranking. period: 'day', 'week', 'month', 'year', 'total' metric: 'cpu', 'mem', 'net' """ now = datetime.now() today = now.strftime('%Y-%m-%d') this_week = now.strftime('%G-W%V') this_month = now.strftime('%Y-%m') this_year = now.strftime('%Y') conn = _get_db() c = conn.cursor() if period == 'day': table = 'daily_rollup' col = 'day' val = today elif period == 'week': table = 'weekly_rollup' col = 'week' val = this_week elif period == 'month': table = 'monthly_rollup' col = 'month' val = this_month elif period == 'year': table = 'yearly_rollup' col = 'year' val = this_year else: # total - from snapshots aggregate all time table = 'snapshots' col = None val = None if metric == 'cpu': order_col = 'cpu_avg' elif metric == 'mem': order_col = 'mem_avg' else: # net order_col = '(net_rx + net_tx)' if period == 'total': c.execute(f''' SELECT app, ROUND(AVG(cpu_pct), 1) as cpu_avg, ROUND(AVG(mem_mb), 1) as mem_avg, MAX(net_rx) as net_rx, MAX(net_tx) as net_tx FROM snapshots GROUP BY app ORDER BY {order_col} DESC LIMIT ? ''', (limit,)) else: c.execute(f''' SELECT app, cpu_avg, mem_avg, net_rx, net_tx FROM {table} WHERE {col} = ? ORDER BY {order_col} DESC LIMIT ? ''', (val, limit)) rows = c.fetchall() conn.close() result = [] for row in rows: app, cpu, mem, rx, tx = row result.append({ 'app': app, 'cpu': cpu or 0, 'mem': mem or 0, 'net_rx': rx or 0, 'net_tx': tx or 0, 'net_total': (rx or 0) + (tx or 0), }) return result def get_combined_ranking(period='total', limit=15): """Combined ranking: weighted score across CPU + mem + net.""" apps = {} periods = ['day', 'week', 'month', 'year', 'total'] target = period if period != 'total' else 'total' for metric in ['cpu', 'mem', 'net']: ranking = get_app_ranking(target, metric, limit=50) total_score = max((r.get('cpu', 0) if metric == 'cpu' else r.get('mem', 0) if metric == 'mem' else r.get('net_total', 0)) for r in ranking) if ranking else 1 if total_score == 0: total_score = 1 for r in ranking: app = r['app'] if app not in apps: apps[app] = {'app': app, 'cpu': 0, 'mem': 0, 'net_rx': 0, 'net_tx': 0, 'net_total': 0, 'score': 0} apps[app]['cpu'] = max(apps[app]['cpu'], r.get('cpu', 0)) apps[app]['mem'] = max(apps[app]['mem'], r.get('mem', 0)) apps[app]['net_rx'] = max(apps[app]['net_rx'], r.get('net_rx', 0)) apps[app]['net_tx'] = max(apps[app]['net_tx'], r.get('net_tx', 0)) apps[app]['net_total'] = max(apps[app]['net_total'], r.get('net_total', 0)) # Weighted score val = (r.get('cpu', 0) if metric == 'cpu' else r.get('mem', 0) if metric == 'mem' else r.get('net_total', 0)) apps[app]['score'] += val / total_score # Sort by score result = sorted(apps.values(), key=lambda x: x['score'], reverse=True)[:limit] return result if __name__ == '__main__': collect_snapshot() print("=== CPU Ranking (total) ===") for r in get_app_ranking('total', 'cpu', 10): print(f" {r['app']:30s} CPU:{r['cpu']:6.1f}% MEM:{r['mem']:8.1f}MB") print("\n=== Combined Ranking ===") for r in get_combined_ranking('total', 10): print(f" {r['app']:30s} score:{r['score']:5.2f} CPU:{r['cpu']:5.1f}% MEM:{r['mem']:7.1f}MB net:{r['net_total']}")