init: fnmon - fnOS 系统监控面板
This commit is contained in:
commit
9717cffb79
12 files changed
+3096
No files matched your search
@@ -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))
|
||||
Reference in new issue
Block a user