init: fnmon - fnOS 系统监控面板

This commit is contained in:
jinghui committed 2026-06-15 23:01:08 +08:00
commit 9717cffb79
12 files changed
+3096

No files matched your search

+231
View File
@@ -0,0 +1,231 @@
#!/usr/bin/env python3
"""Traffic tracker: total / daily / monthly cumulative counters + per-container stats with speed."""
import json
import os
import re
import sqlite3
import subprocess
import time
from datetime import date
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'traffic.db')
# Module-level cache for per-container speed calculation
# { container_name: { 'rx': int, 'tx': int, 'ts': float }, ... }
_PREV_CONTAINER_STATS = {}
def _get_db():
"""Get SQLite connection with schema init."""
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute('''CREATE TABLE IF NOT EXISTS traffic_base (
id TEXT PRIMARY KEY, rx_base INTEGER, tx_base INTEGER
)''')
c.execute('''CREATE TABLE IF NOT EXISTS traffic_daily (
date TEXT PRIMARY KEY, rx_base INTEGER, tx_base INTEGER
)''')
c.execute('''CREATE TABLE IF NOT EXISTS traffic_monthly (
month TEXT PRIMARY KEY, rx_base INTEGER, tx_base INTEGER
)''')
conn.commit()
return conn
def _get_interfaces():
"""Read /proc/net/dev and return {iface: {rx, tx}} for all relevant interfaces."""
interfaces = {}
try:
with open('/proc/net/dev') as f:
for line in f.readlines()[2:]: # skip headers
parts = line.strip().split()
if len(parts) >= 10:
iface = parts[0].rstrip(':')
# Skip loopback
if iface == 'lo':
continue
# Skip veth (Docker internal — counted per-container)
if iface.startswith('veth'):
continue
# Skip individual Docker bridges
if re.match(r'^br-[a-f0-9]{12}$', iface):
continue
interfaces[iface] = {
'rx': int(parts[1]),
'tx': int(parts[9]),
}
except Exception:
pass
return interfaces
def _get_main_traffic(interfaces):
"""Aggregate main interfaces (eno, enx, eth)."""
rx = tx = 0
for iface, data in interfaces.items():
if iface.startswith(('eno', 'enx', 'eth')):
rx += data['rx']
tx += data['tx']
return rx, tx
def _parse_bytes(s):
"""Parse '1.23kB', '45.4MB', '960B' etc to bytes."""
s = s.strip().replace(',', '')
m = re.match(r'([\d.]+)\s*([a-zA-Z]+)', s)
if not m:
return 0
val = float(m.group(1))
unit = m.group(2).lower()
multipliers = {'b': 1, 'kb': 1000, 'kib': 1024, 'mb': 1e6, 'mib': 1048576,
'gb': 1e9, 'gib': 1073741824, 'tb': 1e12, 'tib': 1099511627776}
return int(val * multipliers.get(unit, 1))
def _get_docker_containers():
"""
Get per-container network traffic + real-time speed.
Returns list sorted by total traffic descending.
Results cached for 30 seconds to avoid blocking SSE.
"""
global _PREV_CONTAINER_STATS, _DOCKER_CACHE, _DOCKER_CACHE_TIME
now = time.time()
# Return cache if fresh (30s)
if '_DOCKER_CACHE' in globals() and now - _DOCKER_CACHE_TIME < 30:
return _DOCKER_CACHE
containers = []
try:
result = subprocess.run(
['docker', 'stats', '--no-stream', '--format',
'{{.Name}}\t{{.NetIO}}'],
capture_output=True, text=True, timeout=30
)
lines = result.stdout.strip().split('\n')
now = time.time()
current_snapshot = {}
for line in lines:
parts = line.strip().split('\t')
if len(parts) >= 2:
name = parts[0]
netio = parts[1].strip()
rx_str, tx_str = '', ''
if ' / ' in netio:
rx_str, tx_str = netio.split(' / ', 1)
rx = _parse_bytes(rx_str.strip())
tx = _parse_bytes(tx_str.strip())
current_snapshot[name] = {'rx': rx, 'tx': tx, 'ts': now}
# Compute speed from previous snapshot
rx_speed = tx_speed = 0
prev = _PREV_CONTAINER_STATS.get(name)
if prev:
dt = now - prev['ts']
if dt > 0:
rx_speed = max(0, (rx - prev['rx']) / dt)
tx_speed = max(0, (tx - prev['tx']) / dt)
containers.append({
'name': name,
'rx': rx,
'tx': tx,
'rx_speed': round(rx_speed),
'tx_speed': round(tx_speed),
})
_PREV_CONTAINER_STATS = current_snapshot
except subprocess.TimeoutExpired:
pass
except FileNotFoundError:
pass # Docker not available
except Exception:
pass
# Sort by total traffic (rx + tx) descending
containers.sort(key=lambda c: c['rx'] + c['tx'], reverse=True)
_DOCKER_CACHE = containers
_DOCKER_CACHE_TIME = time.time()
return containers
def get_traffic_stats():
"""
Return traffic stats:
- total: cumulative since tracking started
- daily: cumulative for today
- monthly: cumulative for this month
- containers: top Docker containers with cumulative + speed
"""
conn = _get_db()
c = conn.cursor()
today = date.today().isoformat()
this_month = today[:7] # YYYY-MM
interfaces = _get_interfaces()
main_rx, main_tx = _get_main_traffic(interfaces)
# ---- Total traffic baseline (set once) ----
c.execute('SELECT rx_base, tx_base FROM traffic_base WHERE id=?', ('_main',))
row = c.fetchone()
if row:
rx_base, tx_base = row
else:
rx_base, tx_base = main_rx, main_tx
c.execute('INSERT OR REPLACE INTO traffic_base VALUES (?,?,?)',
('_main', rx_base, tx_base))
conn.commit()
total_rx = max(0, main_rx - rx_base)
total_tx = max(0, main_tx - tx_base)
# ---- Daily cumulative (baseline at start of day) ----
c.execute('SELECT rx_base, tx_base FROM traffic_daily WHERE date=?', (today,))
row = c.fetchone()
if row:
day_rx_base, day_tx_base = row
else:
# First call today: set baseline = current total, so daily starts at 0
day_rx_base, day_tx_base = total_rx, total_tx
c.execute('INSERT OR REPLACE INTO traffic_daily VALUES (?,?,?)',
(today, day_rx_base, day_tx_base))
conn.commit()
daily_rx = max(0, total_rx - day_rx_base)
daily_tx = max(0, total_tx - day_tx_base)
# ---- Monthly cumulative (baseline at start of month) ----
c.execute('SELECT rx_base, tx_base FROM traffic_monthly WHERE month=?',
(this_month,))
row = c.fetchone()
if row:
mon_rx_base, mon_tx_base = row
else:
mon_rx_base, mon_tx_base = total_rx, total_tx
c.execute('INSERT OR REPLACE INTO traffic_monthly VALUES (?,?,?)',
(this_month, mon_rx_base, mon_tx_base))
conn.commit()
monthly_rx = max(0, total_rx - mon_rx_base)
monthly_tx = max(0, total_tx - mon_tx_base)
conn.close()
# ---- Docker containers ----
containers = _get_docker_containers()
return {
'total_rx': total_rx,
'total_tx': total_tx,
'daily_rx': daily_rx,
'daily_tx': daily_tx,
'monthly_rx': monthly_rx,
'monthly_tx': monthly_tx,
'containers': containers[:15], # top 15
}
if __name__ == '__main__':
print(json.dumps(get_traffic_stats(), indent=2))