232 lines
7.4 KiB
Python
232 lines
7.4 KiB
Python
#!/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))
|