mirror of
https://github.com/varun-r-mallya/Python-BPF.git
synced 2025-12-31 21:06:25 +00:00
Merge pull request #75 from pythonbpf/fix-vmlinux-ir-gen
add container-monitor example
This commit is contained in:
49
BCC-Examples/container-monitor/README.md
Normal file
49
BCC-Examples/container-monitor/README.md
Normal file
@ -0,0 +1,49 @@
|
||||
# Container Monitor TUI
|
||||
|
||||
A beautiful terminal-based container monitoring tool that combines syscall tracking, file I/O monitoring, and network traffic analysis using eBPF.
|
||||
|
||||
## Features
|
||||
|
||||
- 🎯 **Interactive Cgroup Selection** - Navigate and select cgroups with arrow keys
|
||||
- 📊 **Real-time Monitoring** - Live graphs and statistics
|
||||
- 🔥 **Syscall Tracking** - Total syscall count per cgroup
|
||||
- 💾 **File I/O Monitoring** - Read/write operations and bytes with graphs
|
||||
- 🌐 **Network Traffic** - RX/TX packets and bytes with live graphs
|
||||
- ⚡ **Efficient Caching** - Reduced /proc lookups for better performance
|
||||
- 🎨 **Beautiful TUI** - Clean, colorful terminal interface
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.7+
|
||||
- pythonbpf
|
||||
- Root privileges (for eBPF)
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Ensure you have pythonbpf installed
|
||||
pip install pythonbpf
|
||||
|
||||
# Run the monitor
|
||||
sudo $(which python) container_monitor.py
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
1. **Selection Screen**: Use ↑↓ arrow keys to navigate through cgroups, press ENTER to select
|
||||
2. **Monitoring Screen**: View real-time graphs and statistics, press ESC or 'b' to go back
|
||||
3. **Exit**: Press 'q' at any time to quit
|
||||
|
||||
## Architecture
|
||||
|
||||
- `container_monitor.py` - Main BPF program combining all three tracers
|
||||
- `data_collector.py` - Data collection, caching, and history management
|
||||
- `tui. py` - Terminal user interface with selection and monitoring screens
|
||||
|
||||
## BPF Programs
|
||||
|
||||
- **vfs_read/vfs_write** - Track file I/O operations
|
||||
- **__netif_receive_skb/__dev_queue_xmit** - Track network traffic
|
||||
- **raw_syscalls/sys_enter** - Count all syscalls
|
||||
|
||||
All programs filter by cgroup ID for per-container monitoring.
|
||||
220
BCC-Examples/container-monitor/container_monitor.py
Normal file
220
BCC-Examples/container-monitor/container_monitor.py
Normal file
@ -0,0 +1,220 @@
|
||||
"""Container Monitor - TUI-based cgroup monitoring combining syscall, file I/O, and network tracking."""
|
||||
|
||||
from pythonbpf import bpf, map, section, bpfglobal, struct, BPF
|
||||
from pythonbpf.maps import HashMap
|
||||
from pythonbpf.helper import get_current_cgroup_id
|
||||
from ctypes import c_int32, c_uint64, c_void_p
|
||||
from vmlinux import struct_pt_regs, struct_sk_buff
|
||||
|
||||
from data_collection import ContainerDataCollector
|
||||
from tui import ContainerMonitorTUI
|
||||
|
||||
|
||||
# ==================== BPF Structs ====================
|
||||
|
||||
|
||||
@bpf
|
||||
@struct
|
||||
class read_stats:
|
||||
bytes: c_uint64
|
||||
ops: c_uint64
|
||||
|
||||
|
||||
@bpf
|
||||
@struct
|
||||
class write_stats:
|
||||
bytes: c_uint64
|
||||
ops: c_uint64
|
||||
|
||||
|
||||
@bpf
|
||||
@struct
|
||||
class net_stats:
|
||||
rx_packets: c_uint64
|
||||
tx_packets: c_uint64
|
||||
rx_bytes: c_uint64
|
||||
tx_bytes: c_uint64
|
||||
|
||||
|
||||
# ==================== BPF Maps ====================
|
||||
|
||||
|
||||
@bpf
|
||||
@map
|
||||
def read_map() -> HashMap:
|
||||
return HashMap(key=c_uint64, value=read_stats, max_entries=1024)
|
||||
|
||||
|
||||
@bpf
|
||||
@map
|
||||
def write_map() -> HashMap:
|
||||
return HashMap(key=c_uint64, value=write_stats, max_entries=1024)
|
||||
|
||||
|
||||
@bpf
|
||||
@map
|
||||
def net_stats_map() -> HashMap:
|
||||
return HashMap(key=c_uint64, value=net_stats, max_entries=1024)
|
||||
|
||||
|
||||
@bpf
|
||||
@map
|
||||
def syscall_count() -> HashMap:
|
||||
return HashMap(key=c_uint64, value=c_uint64, max_entries=1024)
|
||||
|
||||
|
||||
# ==================== File I/O Tracing ====================
|
||||
|
||||
|
||||
@bpf
|
||||
@section("kprobe/vfs_read")
|
||||
def trace_read(ctx: struct_pt_regs) -> c_int32:
|
||||
cg = get_current_cgroup_id()
|
||||
count = c_uint64(ctx.dx)
|
||||
ptr = read_map.lookup(cg)
|
||||
if ptr:
|
||||
s = read_stats()
|
||||
s.bytes = ptr.bytes + count
|
||||
s.ops = ptr.ops + 1
|
||||
read_map.update(cg, s)
|
||||
else:
|
||||
s = read_stats()
|
||||
s.bytes = count
|
||||
s.ops = c_uint64(1)
|
||||
read_map.update(cg, s)
|
||||
|
||||
return c_int32(0)
|
||||
|
||||
|
||||
@bpf
|
||||
@section("kprobe/vfs_write")
|
||||
def trace_write(ctx1: struct_pt_regs) -> c_int32:
|
||||
cg = get_current_cgroup_id()
|
||||
count = c_uint64(ctx1.dx)
|
||||
ptr = write_map.lookup(cg)
|
||||
|
||||
if ptr:
|
||||
s = write_stats()
|
||||
s.bytes = ptr.bytes + count
|
||||
s.ops = ptr.ops + 1
|
||||
write_map.update(cg, s)
|
||||
else:
|
||||
s = write_stats()
|
||||
s.bytes = count
|
||||
s.ops = c_uint64(1)
|
||||
write_map.update(cg, s)
|
||||
|
||||
return c_int32(0)
|
||||
|
||||
|
||||
# ==================== Network I/O Tracing ====================
|
||||
|
||||
|
||||
@bpf
|
||||
@section("kprobe/__netif_receive_skb")
|
||||
def trace_netif_rx(ctx2: struct_pt_regs) -> c_int32:
|
||||
cgroup_id = get_current_cgroup_id()
|
||||
skb = struct_sk_buff(ctx2.di)
|
||||
pkt_len = c_uint64(skb.len)
|
||||
|
||||
stats_ptr = net_stats_map.lookup(cgroup_id)
|
||||
|
||||
if stats_ptr:
|
||||
stats = net_stats()
|
||||
stats.rx_packets = stats_ptr.rx_packets + 1
|
||||
stats.tx_packets = stats_ptr.tx_packets
|
||||
stats.rx_bytes = stats_ptr.rx_bytes + pkt_len
|
||||
stats.tx_bytes = stats_ptr.tx_bytes
|
||||
net_stats_map.update(cgroup_id, stats)
|
||||
else:
|
||||
stats = net_stats()
|
||||
stats.rx_packets = c_uint64(1)
|
||||
stats.tx_packets = c_uint64(0)
|
||||
stats.rx_bytes = pkt_len
|
||||
stats.tx_bytes = c_uint64(0)
|
||||
net_stats_map.update(cgroup_id, stats)
|
||||
|
||||
return c_int32(0)
|
||||
|
||||
|
||||
@bpf
|
||||
@section("kprobe/__dev_queue_xmit")
|
||||
def trace_dev_xmit(ctx3: struct_pt_regs) -> c_int32:
|
||||
cgroup_id = get_current_cgroup_id()
|
||||
skb = struct_sk_buff(ctx3.di)
|
||||
pkt_len = c_uint64(skb.len)
|
||||
|
||||
stats_ptr = net_stats_map.lookup(cgroup_id)
|
||||
|
||||
if stats_ptr:
|
||||
stats = net_stats()
|
||||
stats.rx_packets = stats_ptr.rx_packets
|
||||
stats.tx_packets = stats_ptr.tx_packets + 1
|
||||
stats.rx_bytes = stats_ptr.rx_bytes
|
||||
stats.tx_bytes = stats_ptr.tx_bytes + pkt_len
|
||||
net_stats_map.update(cgroup_id, stats)
|
||||
else:
|
||||
stats = net_stats()
|
||||
stats.rx_packets = c_uint64(0)
|
||||
stats.tx_packets = c_uint64(1)
|
||||
stats.rx_bytes = c_uint64(0)
|
||||
stats.tx_bytes = pkt_len
|
||||
net_stats_map.update(cgroup_id, stats)
|
||||
|
||||
return c_int32(0)
|
||||
|
||||
|
||||
# ==================== Syscall Tracing ====================
|
||||
|
||||
|
||||
@bpf
|
||||
@section("tracepoint/raw_syscalls/sys_enter")
|
||||
def count_syscalls(ctx: c_void_p) -> c_int32:
|
||||
cgroup_id = get_current_cgroup_id()
|
||||
count_ptr = syscall_count.lookup(cgroup_id)
|
||||
|
||||
if count_ptr:
|
||||
new_count = count_ptr + c_uint64(1)
|
||||
syscall_count.update(cgroup_id, new_count)
|
||||
else:
|
||||
syscall_count.update(cgroup_id, c_uint64(1))
|
||||
|
||||
return c_int32(0)
|
||||
|
||||
|
||||
@bpf
|
||||
@bpfglobal
|
||||
def LICENSE() -> str:
|
||||
return "GPL"
|
||||
|
||||
|
||||
# ==================== Main ====================
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🔥 Loading BPF programs...")
|
||||
|
||||
# Load and attach BPF program
|
||||
b = BPF()
|
||||
b.load()
|
||||
b.attach_all()
|
||||
|
||||
# Get map references and enable struct deserialization
|
||||
read_map_ref = b["read_map"]
|
||||
write_map_ref = b["write_map"]
|
||||
net_stats_map_ref = b["net_stats_map"]
|
||||
syscall_count_ref = b["syscall_count"]
|
||||
|
||||
read_map_ref.set_value_struct("read_stats")
|
||||
write_map_ref.set_value_struct("write_stats")
|
||||
net_stats_map_ref.set_value_struct("net_stats")
|
||||
|
||||
print("✅ BPF programs loaded and attached")
|
||||
|
||||
# Setup data collector
|
||||
collector = ContainerDataCollector(
|
||||
read_map_ref, write_map_ref, net_stats_map_ref, syscall_count_ref
|
||||
)
|
||||
|
||||
# Create and run TUI
|
||||
tui = ContainerMonitorTUI(collector)
|
||||
tui.run()
|
||||
208
BCC-Examples/container-monitor/data_collection.py
Normal file
208
BCC-Examples/container-monitor/data_collection.py
Normal file
@ -0,0 +1,208 @@
|
||||
"""Data collection and management for container monitoring."""
|
||||
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Set, Optional
|
||||
from dataclasses import dataclass
|
||||
from collections import deque, defaultdict
|
||||
|
||||
|
||||
@dataclass
|
||||
class CgroupInfo:
|
||||
"""Information about a cgroup."""
|
||||
|
||||
id: int
|
||||
name: str
|
||||
path: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class ContainerStats:
|
||||
"""Statistics for a container/cgroup."""
|
||||
|
||||
cgroup_id: int
|
||||
cgroup_name: str
|
||||
|
||||
# File I/O
|
||||
read_ops: int = 0
|
||||
read_bytes: int = 0
|
||||
write_ops: int = 0
|
||||
write_bytes: int = 0
|
||||
|
||||
# Network I/O
|
||||
rx_packets: int = 0
|
||||
rx_bytes: int = 0
|
||||
tx_packets: int = 0
|
||||
tx_bytes: int = 0
|
||||
|
||||
# Syscalls
|
||||
syscall_count: int = 0
|
||||
|
||||
# Timestamp
|
||||
timestamp: float = 0.0
|
||||
|
||||
|
||||
class ContainerDataCollector:
|
||||
"""Collects and manages container monitoring data from BPF."""
|
||||
|
||||
def __init__(
|
||||
self, read_map, write_map, net_stats_map, syscall_map, history_size: int = 100
|
||||
):
|
||||
self.read_map = read_map
|
||||
self.write_map = write_map
|
||||
self.net_stats_map = net_stats_map
|
||||
self.syscall_map = syscall_map
|
||||
|
||||
# Caching
|
||||
self._cgroup_cache: Dict[int, CgroupInfo] = {}
|
||||
self._cgroup_cache_time = 0
|
||||
self._cache_ttl = 5.0
|
||||
0 # Refresh cache every 5 seconds
|
||||
|
||||
# Historical data for graphing
|
||||
self._history_size = history_size
|
||||
self._history: Dict[int, deque] = defaultdict(
|
||||
lambda: deque(maxlen=history_size)
|
||||
)
|
||||
|
||||
def get_all_cgroups(self) -> List[CgroupInfo]:
|
||||
"""Get all cgroups with caching."""
|
||||
current_time = time.time()
|
||||
|
||||
# Use cached data if still valid
|
||||
if current_time - self._cgroup_cache_time < self._cache_ttl:
|
||||
return list(self._cgroup_cache.values())
|
||||
|
||||
# Refresh cache
|
||||
self._refresh_cgroup_cache()
|
||||
return list(self._cgroup_cache.values())
|
||||
|
||||
def _refresh_cgroup_cache(self):
|
||||
"""Refresh the cgroup cache from /proc."""
|
||||
cgroup_map: Dict[int, Set[str]] = defaultdict(set)
|
||||
|
||||
# Scan /proc to find all cgroups
|
||||
for proc_dir in Path("/proc").glob("[0-9]*"):
|
||||
try:
|
||||
cgroup_file = proc_dir / "cgroup"
|
||||
if not cgroup_file.exists():
|
||||
continue
|
||||
|
||||
with open(cgroup_file) as f:
|
||||
for line in f:
|
||||
parts = line.strip().split(":")
|
||||
if len(parts) >= 3:
|
||||
cgroup_path = parts[2]
|
||||
cgroup_mount = f"/sys/fs/cgroup{cgroup_path}"
|
||||
|
||||
if os.path.exists(cgroup_mount):
|
||||
stat_info = os.stat(cgroup_mount)
|
||||
cgroup_id = stat_info.st_ino
|
||||
cgroup_map[cgroup_id].add(cgroup_path)
|
||||
|
||||
except (PermissionError, FileNotFoundError, OSError):
|
||||
continue
|
||||
|
||||
# Update cache with best names
|
||||
new_cache = {}
|
||||
for cgroup_id, paths in cgroup_map.items():
|
||||
# Pick the most descriptive path
|
||||
best_path = self._get_best_cgroup_path(paths)
|
||||
name = self._get_cgroup_name(best_path)
|
||||
|
||||
new_cache[cgroup_id] = CgroupInfo(id=cgroup_id, name=name, path=best_path)
|
||||
|
||||
self._cgroup_cache = new_cache
|
||||
self._cgroup_cache_time = time.time()
|
||||
|
||||
def _get_best_cgroup_path(self, paths: Set[str]) -> str:
|
||||
"""Select the most descriptive cgroup path."""
|
||||
path_list = list(paths)
|
||||
|
||||
# Prefer paths with more components (more specific)
|
||||
# Prefer paths containing docker, podman, etc.
|
||||
for keyword in ["docker", "podman", "kubernetes", "k8s", "systemd"]:
|
||||
for path in path_list:
|
||||
if keyword in path.lower():
|
||||
return path
|
||||
|
||||
# Return longest path (most specific)
|
||||
return max(path_list, key=lambda p: (len(p.split("/")), len(p)))
|
||||
|
||||
def _get_cgroup_name(self, path: str) -> str:
|
||||
"""Extract a friendly name from cgroup path."""
|
||||
if not path or path == "/":
|
||||
return "root"
|
||||
|
||||
# Remove leading/trailing slashes
|
||||
path = path.strip("/")
|
||||
|
||||
# Try to extract container ID or service name
|
||||
parts = path.split("/")
|
||||
|
||||
# For Docker: /docker/<container_id>
|
||||
if "docker" in path.lower():
|
||||
for i, part in enumerate(parts):
|
||||
if part.lower() == "docker" and i + 1 < len(parts):
|
||||
container_id = parts[i + 1][:12] # Short ID
|
||||
return f"docker:{container_id}"
|
||||
|
||||
# For systemd services
|
||||
if "system.slice" in path:
|
||||
for part in parts:
|
||||
if part.endswith(".service"):
|
||||
return part.replace(".service", "")
|
||||
|
||||
# For user slices
|
||||
if "user.slice" in path:
|
||||
return f"user:{parts[-1]}" if parts else "user"
|
||||
|
||||
# Default: use last component
|
||||
return parts[-1] if parts else path
|
||||
|
||||
def get_stats_for_cgroup(self, cgroup_id: int) -> ContainerStats:
|
||||
"""Get current statistics for a specific cgroup."""
|
||||
cgroup_info = self._cgroup_cache.get(cgroup_id)
|
||||
cgroup_name = cgroup_info.name if cgroup_info else f"cgroup-{cgroup_id}"
|
||||
|
||||
stats = ContainerStats(
|
||||
cgroup_id=cgroup_id, cgroup_name=cgroup_name, timestamp=time.time()
|
||||
)
|
||||
|
||||
# Get file I/O stats
|
||||
read_stat = self.read_map.lookup(cgroup_id)
|
||||
if read_stat:
|
||||
stats.read_ops = int(read_stat.ops)
|
||||
stats.read_bytes = int(read_stat.bytes)
|
||||
|
||||
write_stat = self.write_map.lookup(cgroup_id)
|
||||
if write_stat:
|
||||
stats.write_ops = int(write_stat.ops)
|
||||
stats.write_bytes = int(write_stat.bytes)
|
||||
|
||||
# Get network stats
|
||||
net_stat = self.net_stats_map.lookup(cgroup_id)
|
||||
if net_stat:
|
||||
stats.rx_packets = int(net_stat.rx_packets)
|
||||
stats.rx_bytes = int(net_stat.rx_bytes)
|
||||
stats.tx_packets = int(net_stat.tx_packets)
|
||||
stats.tx_bytes = int(net_stat.tx_bytes)
|
||||
|
||||
# Get syscall count
|
||||
syscall_cnt = self.syscall_map.lookup(cgroup_id)
|
||||
if syscall_cnt is not None:
|
||||
stats.syscall_count = int(syscall_cnt)
|
||||
|
||||
# Add to history
|
||||
self._history[cgroup_id].append(stats)
|
||||
|
||||
return stats
|
||||
|
||||
def get_history(self, cgroup_id: int) -> List[ContainerStats]:
|
||||
"""Get historical statistics for graphing."""
|
||||
return list(self._history[cgroup_id])
|
||||
|
||||
def get_cgroup_info(self, cgroup_id: int) -> Optional[CgroupInfo]:
|
||||
"""Get cached cgroup information."""
|
||||
return self._cgroup_cache.get(cgroup_id)
|
||||
527
BCC-Examples/container-monitor/tui.py
Normal file
527
BCC-Examples/container-monitor/tui.py
Normal file
@ -0,0 +1,527 @@
|
||||
"""Terminal User Interface for container monitoring."""
|
||||
|
||||
import time
|
||||
import curses
|
||||
from typing import Optional, List
|
||||
from data_collection import ContainerDataCollector
|
||||
|
||||
|
||||
class ContainerMonitorTUI:
|
||||
"""TUI for container monitoring with cgroup selection and live graphs."""
|
||||
|
||||
def __init__(self, collector: ContainerDataCollector):
|
||||
self.collector = collector
|
||||
self.selected_cgroup: Optional[int] = None
|
||||
self.current_screen = "selection" # "selection" or "monitoring"
|
||||
self.selected_index = 0
|
||||
self.scroll_offset = 0
|
||||
|
||||
def run(self):
|
||||
"""Run the TUI application."""
|
||||
curses.wrapper(self._main_loop)
|
||||
|
||||
def _main_loop(self, stdscr):
|
||||
"""Main curses loop."""
|
||||
# Configure curses
|
||||
curses.curs_set(0) # Hide cursor
|
||||
stdscr.nodelay(True) # Non-blocking input
|
||||
stdscr.timeout(100) # Refresh every 100ms
|
||||
|
||||
# Initialize colors
|
||||
curses.start_color()
|
||||
curses.init_pair(1, curses.COLOR_CYAN, curses.COLOR_BLACK)
|
||||
curses.init_pair(2, curses.COLOR_GREEN, curses.COLOR_BLACK)
|
||||
curses.init_pair(3, curses.COLOR_YELLOW, curses.COLOR_BLACK)
|
||||
curses.init_pair(4, curses.COLOR_RED, curses.COLOR_BLACK)
|
||||
curses.init_pair(5, curses.COLOR_MAGENTA, curses.COLOR_BLACK)
|
||||
curses.init_pair(6, curses.COLOR_WHITE, curses.COLOR_BLUE)
|
||||
curses.init_pair(7, curses.COLOR_BLUE, curses.COLOR_BLACK)
|
||||
curses.init_pair(8, curses.COLOR_WHITE, curses.COLOR_CYAN)
|
||||
|
||||
while True:
|
||||
stdscr.clear()
|
||||
|
||||
try:
|
||||
if self.current_screen == "selection":
|
||||
self._draw_selection_screen(stdscr)
|
||||
elif self.current_screen == "monitoring":
|
||||
self._draw_monitoring_screen(stdscr)
|
||||
|
||||
stdscr.refresh()
|
||||
|
||||
# Handle input
|
||||
key = stdscr.getch()
|
||||
if key != -1:
|
||||
if not self._handle_input(key):
|
||||
break # Exit requested
|
||||
|
||||
except KeyboardInterrupt:
|
||||
break
|
||||
except Exception as e:
|
||||
# Show error
|
||||
stdscr.addstr(0, 0, f"Error: {str(e)}")
|
||||
stdscr.refresh()
|
||||
time.sleep(2)
|
||||
|
||||
def _draw_selection_screen(self, stdscr):
|
||||
"""Draw the cgroup selection screen."""
|
||||
height, width = stdscr.getmaxyx()
|
||||
|
||||
# Draw fancy header box
|
||||
self._draw_fancy_header(
|
||||
stdscr, "🐳 CONTAINER MONITOR", "Select a Cgroup to Monitor"
|
||||
)
|
||||
|
||||
# Instructions
|
||||
instructions = "↑↓: Navigate | ENTER: Select | q: Quit | r: Refresh"
|
||||
stdscr.attron(curses.color_pair(3))
|
||||
stdscr.addstr(3, (width - len(instructions)) // 2, instructions)
|
||||
stdscr.attroff(curses.color_pair(3))
|
||||
|
||||
# Get cgroups
|
||||
cgroups = self.collector.get_all_cgroups()
|
||||
|
||||
if not cgroups:
|
||||
msg = "No cgroups found. Waiting for activity..."
|
||||
stdscr.attron(curses.color_pair(4))
|
||||
stdscr.addstr(height // 2, (width - len(msg)) // 2, msg)
|
||||
stdscr.attroff(curses.color_pair(4))
|
||||
return
|
||||
|
||||
# Sort cgroups by name
|
||||
cgroups.sort(key=lambda c: c.name)
|
||||
|
||||
# Adjust selection bounds
|
||||
if self.selected_index >= len(cgroups):
|
||||
self.selected_index = len(cgroups) - 1
|
||||
if self.selected_index < 0:
|
||||
self.selected_index = 0
|
||||
|
||||
# Calculate visible range
|
||||
list_height = height - 8
|
||||
if self.selected_index < self.scroll_offset:
|
||||
self.scroll_offset = self.selected_index
|
||||
elif self.selected_index >= self.scroll_offset + list_height:
|
||||
self.scroll_offset = self.selected_index - list_height + 1
|
||||
|
||||
# Draw cgroup list with fancy borders
|
||||
start_y = 5
|
||||
stdscr.attron(curses.color_pair(1))
|
||||
stdscr.addstr(start_y, 2, "╔" + "═" * (width - 6) + "╗")
|
||||
stdscr.attroff(curses.color_pair(1))
|
||||
|
||||
for i in range(list_height):
|
||||
idx = self.scroll_offset + i
|
||||
y = start_y + 1 + i
|
||||
|
||||
stdscr.attron(curses.color_pair(1))
|
||||
stdscr.addstr(y, 2, "║")
|
||||
stdscr.addstr(y, width - 3, "║")
|
||||
stdscr.attroff(curses.color_pair(1))
|
||||
|
||||
if idx >= len(cgroups):
|
||||
continue
|
||||
|
||||
cgroup = cgroups[idx]
|
||||
|
||||
if idx == self.selected_index:
|
||||
# Highlight selected with better styling
|
||||
stdscr.attron(curses.color_pair(8) | curses.A_BOLD)
|
||||
line = f" ► {cgroup.name:<35} │ ID: {cgroup.id} "
|
||||
stdscr.addstr(y, 3, line[: width - 6])
|
||||
stdscr.attroff(curses.color_pair(8) | curses.A_BOLD)
|
||||
else:
|
||||
stdscr.attron(curses.color_pair(7))
|
||||
line = f" {cgroup.name:<35} │ ID: {cgroup.id} "
|
||||
stdscr.addstr(y, 3, line[: width - 6])
|
||||
stdscr.attroff(curses.color_pair(7))
|
||||
|
||||
# Bottom border
|
||||
bottom_y = start_y + 1 + list_height
|
||||
stdscr.attron(curses.color_pair(1))
|
||||
stdscr.addstr(bottom_y, 2, "╚" + "═" * (width - 6) + "╝")
|
||||
stdscr.attroff(curses.color_pair(1))
|
||||
|
||||
# Footer with count and scroll indicator
|
||||
footer = f"Total: {len(cgroups)} cgroups"
|
||||
if len(cgroups) > list_height:
|
||||
footer += f" │ Showing {self.scroll_offset + 1}-{min(self.scroll_offset + list_height, len(cgroups))}"
|
||||
stdscr.attron(curses.color_pair(1))
|
||||
stdscr.addstr(height - 2, (width - len(footer)) // 2, footer)
|
||||
stdscr.attroff(curses.color_pair(1))
|
||||
|
||||
def _draw_monitoring_screen(self, stdscr):
|
||||
"""Draw the monitoring screen for selected cgroup."""
|
||||
height, width = stdscr.getmaxyx()
|
||||
|
||||
if self.selected_cgroup is None:
|
||||
return
|
||||
|
||||
# Get current stats
|
||||
stats = self.collector.get_stats_for_cgroup(self.selected_cgroup)
|
||||
history = self.collector.get_history(self.selected_cgroup)
|
||||
|
||||
# Draw fancy header
|
||||
self._draw_fancy_header(
|
||||
stdscr, f"📊 {stats.cgroup_name}", "Live Performance Metrics"
|
||||
)
|
||||
|
||||
# Instructions
|
||||
instructions = "ESC/b: Back to List | q: Quit"
|
||||
stdscr.attron(curses.color_pair(3))
|
||||
stdscr.addstr(3, (width - len(instructions)) // 2, instructions)
|
||||
stdscr.attroff(curses.color_pair(3))
|
||||
|
||||
# Calculate metrics for rate display
|
||||
rates = self._calculate_rates(history)
|
||||
|
||||
y = 5
|
||||
|
||||
# Syscall count in a fancy box
|
||||
self._draw_metric_box(
|
||||
stdscr,
|
||||
y,
|
||||
2,
|
||||
width - 4,
|
||||
"⚡ SYSTEM CALLS",
|
||||
f"{stats.syscall_count:,}",
|
||||
f"Rate: {rates['syscalls_per_sec']:.1f}/sec",
|
||||
curses.color_pair(5),
|
||||
)
|
||||
|
||||
y += 4
|
||||
|
||||
# Network I/O Section
|
||||
self._draw_section_header(stdscr, y, "🌐 NETWORK I/O", 1)
|
||||
y += 1
|
||||
|
||||
# RX graph with legend
|
||||
rx_label = f"RX: {self._format_bytes(stats.rx_bytes)}"
|
||||
rx_rate = f"{self._format_bytes(rates['rx_bytes_per_sec'])}/s"
|
||||
rx_pkts = f"{stats.rx_packets:,} pkts ({rates['rx_pkts_per_sec']:.1f}/s)"
|
||||
|
||||
self._draw_labeled_graph(
|
||||
stdscr,
|
||||
y,
|
||||
2,
|
||||
width - 4,
|
||||
4,
|
||||
rx_label,
|
||||
rx_rate,
|
||||
rx_pkts,
|
||||
[s.rx_bytes for s in history],
|
||||
curses.color_pair(2),
|
||||
"Received Traffic (last 100 samples)",
|
||||
)
|
||||
|
||||
y += 6
|
||||
|
||||
# TX graph with legend
|
||||
tx_label = f"TX: {self._format_bytes(stats.tx_bytes)}"
|
||||
tx_rate = f"{self._format_bytes(rates['tx_bytes_per_sec'])}/s"
|
||||
tx_pkts = f"{stats.tx_packets:,} pkts ({rates['tx_pkts_per_sec']:.1f}/s)"
|
||||
|
||||
self._draw_labeled_graph(
|
||||
stdscr,
|
||||
y,
|
||||
2,
|
||||
width - 4,
|
||||
4,
|
||||
tx_label,
|
||||
tx_rate,
|
||||
tx_pkts,
|
||||
[s.tx_bytes for s in history],
|
||||
curses.color_pair(3),
|
||||
"Transmitted Traffic (last 100 samples)",
|
||||
)
|
||||
|
||||
y += 6
|
||||
|
||||
# File I/O Section
|
||||
self._draw_section_header(stdscr, y, "💾 FILE I/O", 1)
|
||||
y += 1
|
||||
|
||||
# Read graph with legend
|
||||
read_label = f"READ: {self._format_bytes(stats.read_bytes)}"
|
||||
read_rate = f"{self._format_bytes(rates['read_bytes_per_sec'])}/s"
|
||||
read_ops = f"{stats.read_ops:,} ops ({rates['read_ops_per_sec']:.1f}/s)"
|
||||
|
||||
self._draw_labeled_graph(
|
||||
stdscr,
|
||||
y,
|
||||
2,
|
||||
width - 4,
|
||||
4,
|
||||
read_label,
|
||||
read_rate,
|
||||
read_ops,
|
||||
[s.read_bytes for s in history],
|
||||
curses.color_pair(4),
|
||||
"Read Operations (last 100 samples)",
|
||||
)
|
||||
|
||||
y += 6
|
||||
|
||||
# Write graph with legend
|
||||
write_label = f"WRITE: {self._format_bytes(stats.write_bytes)}"
|
||||
write_rate = f"{self._format_bytes(rates['write_bytes_per_sec'])}/s"
|
||||
write_ops = f"{stats.write_ops:,} ops ({rates['write_ops_per_sec']:.1f}/s)"
|
||||
|
||||
self._draw_labeled_graph(
|
||||
stdscr,
|
||||
y,
|
||||
2,
|
||||
width - 4,
|
||||
4,
|
||||
write_label,
|
||||
write_rate,
|
||||
write_ops,
|
||||
[s.write_bytes for s in history],
|
||||
curses.color_pair(5),
|
||||
"Write Operations (last 100 samples)",
|
||||
)
|
||||
|
||||
def _draw_fancy_header(self, stdscr, title: str, subtitle: str):
|
||||
"""Draw a fancy header with title and subtitle."""
|
||||
height, width = stdscr.getmaxyx()
|
||||
|
||||
# Top border
|
||||
stdscr.attron(curses.color_pair(6) | curses.A_BOLD)
|
||||
stdscr.addstr(0, 0, "═" * width)
|
||||
|
||||
# Title
|
||||
stdscr.addstr(0, (width - len(title)) // 2, f" {title} ")
|
||||
stdscr.attroff(curses.color_pair(6) | curses.A_BOLD)
|
||||
|
||||
# Subtitle
|
||||
stdscr.attron(curses.color_pair(1))
|
||||
stdscr.addstr(1, (width - len(subtitle)) // 2, subtitle)
|
||||
stdscr.attroff(curses.color_pair(1))
|
||||
|
||||
# Bottom border
|
||||
stdscr.attron(curses.color_pair(6))
|
||||
stdscr.addstr(2, 0, "═" * width)
|
||||
stdscr.attroff(curses.color_pair(6))
|
||||
|
||||
def _draw_metric_box(
|
||||
self,
|
||||
stdscr,
|
||||
y: int,
|
||||
x: int,
|
||||
width: int,
|
||||
label: str,
|
||||
value: str,
|
||||
detail: str,
|
||||
color_pair: int,
|
||||
):
|
||||
"""Draw a fancy box for displaying a metric."""
|
||||
# Top border
|
||||
stdscr.attron(color_pair | curses.A_BOLD)
|
||||
stdscr.addstr(y, x, "┌" + "─" * (width - 2) + "┐")
|
||||
|
||||
# Label
|
||||
stdscr.addstr(y + 1, x, "│")
|
||||
stdscr.addstr(y + 1, x + 2, label)
|
||||
stdscr.addstr(y + 1, x + width - 1, "│")
|
||||
|
||||
# Value (large)
|
||||
stdscr.addstr(y + 2, x, "│")
|
||||
stdscr.attroff(color_pair | curses.A_BOLD)
|
||||
stdscr.attron(curses.color_pair(2) | curses.A_BOLD)
|
||||
stdscr.addstr(y + 2, x + 4, value)
|
||||
stdscr.attroff(curses.color_pair(2) | curses.A_BOLD)
|
||||
stdscr.attron(color_pair | curses.A_BOLD)
|
||||
stdscr.addstr(y + 2, x + width - 1, "│")
|
||||
|
||||
# Detail
|
||||
stdscr.addstr(y + 2, x + width - len(detail) - 3, detail)
|
||||
|
||||
# Bottom border
|
||||
stdscr.addstr(y + 3, x, "└" + "─" * (width - 2) + "┘")
|
||||
stdscr.attroff(color_pair | curses.A_BOLD)
|
||||
|
||||
def _draw_section_header(self, stdscr, y: int, title: str, color_pair: int):
|
||||
"""Draw a section header."""
|
||||
height, width = stdscr.getmaxyx()
|
||||
stdscr.attron(curses.color_pair(color_pair) | curses.A_BOLD)
|
||||
stdscr.addstr(y, 2, title)
|
||||
stdscr.addstr(y, len(title) + 3, "─" * (width - len(title) - 5))
|
||||
stdscr.attroff(curses.color_pair(color_pair) | curses.A_BOLD)
|
||||
|
||||
def _draw_labeled_graph(
|
||||
self,
|
||||
stdscr,
|
||||
y: int,
|
||||
x: int,
|
||||
width: int,
|
||||
height: int,
|
||||
label: str,
|
||||
rate: str,
|
||||
detail: str,
|
||||
data: List[float],
|
||||
color_pair: int,
|
||||
description: str,
|
||||
):
|
||||
"""Draw a graph with labels and legend."""
|
||||
# Header with metrics
|
||||
stdscr.attron(curses.color_pair(1) | curses.A_BOLD)
|
||||
stdscr.addstr(y, x, label)
|
||||
stdscr.attroff(curses.color_pair(1) | curses.A_BOLD)
|
||||
|
||||
stdscr.attron(curses.color_pair(2))
|
||||
stdscr.addstr(y, x + len(label) + 2, rate)
|
||||
stdscr.attroff(curses.color_pair(2))
|
||||
|
||||
stdscr.attron(curses.color_pair(7))
|
||||
stdscr.addstr(y, x + len(label) + len(rate) + 4, detail)
|
||||
stdscr.attroff(curses.color_pair(7))
|
||||
|
||||
# Draw the graph
|
||||
if len(data) > 1:
|
||||
self._draw_bar_graph_enhanced(
|
||||
stdscr, y + 1, x, width, height, data, color_pair
|
||||
)
|
||||
else:
|
||||
stdscr.attron(curses.color_pair(7))
|
||||
stdscr.addstr(y + 2, x + 2, "Collecting data...")
|
||||
stdscr.attroff(curses.color_pair(7))
|
||||
|
||||
# Graph legend at bottom
|
||||
stdscr.attron(curses.color_pair(7))
|
||||
stdscr.addstr(y + height + 1, x, f"└─ {description}")
|
||||
stdscr.attroff(curses.color_pair(7))
|
||||
|
||||
def _draw_bar_graph_enhanced(
|
||||
self,
|
||||
stdscr,
|
||||
y: int,
|
||||
x: int,
|
||||
width: int,
|
||||
height: int,
|
||||
data: List[float],
|
||||
color_pair: int,
|
||||
):
|
||||
"""Draw an enhanced bar graph with axis and scale."""
|
||||
if not data or width < 2:
|
||||
return
|
||||
|
||||
# Calculate statistics
|
||||
max_val = max(data) if max(data) > 0 else 1
|
||||
min_val = min(data)
|
||||
avg_val = sum(data) / len(data)
|
||||
|
||||
# Take last 'width - 10' data points (leave room for Y-axis)
|
||||
graph_width = width - 12
|
||||
recent_data = data[-graph_width:] if len(data) > graph_width else data
|
||||
|
||||
# Draw Y-axis labels
|
||||
stdscr.attron(curses.color_pair(7))
|
||||
stdscr.addstr(y, x, f"│{self._format_bytes(max_val):>9}")
|
||||
stdscr.addstr(y + height // 2, x, f"│{self._format_bytes(avg_val):>9}")
|
||||
stdscr.addstr(y + height - 1, x, f"│{self._format_bytes(min_val):>9}")
|
||||
stdscr.attroff(curses.color_pair(7))
|
||||
|
||||
# Draw bars
|
||||
for row in range(height):
|
||||
threshold = (height - row) / height
|
||||
bar_line = ""
|
||||
|
||||
for val in recent_data:
|
||||
normalized = val / max_val if max_val > 0 else 0
|
||||
if normalized >= threshold:
|
||||
bar_line += "█"
|
||||
elif normalized >= threshold - 0.15:
|
||||
bar_line += "▓"
|
||||
elif normalized >= threshold - 0.35:
|
||||
bar_line += "▒"
|
||||
elif normalized >= threshold - 0.5:
|
||||
bar_line += "░"
|
||||
else:
|
||||
bar_line += " "
|
||||
|
||||
stdscr.attron(color_pair)
|
||||
stdscr.addstr(y + row, x + 11, bar_line)
|
||||
stdscr.attroff(color_pair)
|
||||
|
||||
# Draw X-axis
|
||||
stdscr.attron(curses.color_pair(7))
|
||||
stdscr.addstr(y + height, x + 10, "├" + "─" * len(recent_data))
|
||||
stdscr.addstr(y + height, x + 10 + len(recent_data), "→ time")
|
||||
stdscr.attroff(curses.color_pair(7))
|
||||
|
||||
def _calculate_rates(self, history: List) -> dict:
|
||||
"""Calculate per-second rates from history."""
|
||||
if len(history) < 2:
|
||||
return {
|
||||
"syscalls_per_sec": 0.0,
|
||||
"rx_bytes_per_sec": 0.0,
|
||||
"tx_bytes_per_sec": 0.0,
|
||||
"rx_pkts_per_sec": 0.0,
|
||||
"tx_pkts_per_sec": 0.0,
|
||||
"read_bytes_per_sec": 0.0,
|
||||
"write_bytes_per_sec": 0.0,
|
||||
"read_ops_per_sec": 0.0,
|
||||
"write_ops_per_sec": 0.0,
|
||||
}
|
||||
|
||||
# Calculate delta between last two samples
|
||||
recent = history[-1]
|
||||
previous = history[-2]
|
||||
time_delta = recent.timestamp - previous.timestamp
|
||||
|
||||
if time_delta <= 0:
|
||||
time_delta = 1.0
|
||||
|
||||
return {
|
||||
"syscalls_per_sec": (recent.syscall_count - previous.syscall_count)
|
||||
/ time_delta,
|
||||
"rx_bytes_per_sec": (recent.rx_bytes - previous.rx_bytes) / time_delta,
|
||||
"tx_bytes_per_sec": (recent.tx_bytes - previous.tx_bytes) / time_delta,
|
||||
"rx_pkts_per_sec": (recent.rx_packets - previous.rx_packets) / time_delta,
|
||||
"tx_pkts_per_sec": (recent.tx_packets - previous.tx_packets) / time_delta,
|
||||
"read_bytes_per_sec": (recent.read_bytes - previous.read_bytes)
|
||||
/ time_delta,
|
||||
"write_bytes_per_sec": (recent.write_bytes - previous.write_bytes)
|
||||
/ time_delta,
|
||||
"read_ops_per_sec": (recent.read_ops - previous.read_ops) / time_delta,
|
||||
"write_ops_per_sec": (recent.write_ops - previous.write_ops) / time_delta,
|
||||
}
|
||||
|
||||
def _format_bytes(self, bytes_val: float) -> str:
|
||||
"""Format bytes into human-readable string."""
|
||||
if bytes_val < 0:
|
||||
bytes_val = 0
|
||||
for unit in ["B", "KB", "MB", "GB", "TB"]:
|
||||
if bytes_val < 1024.0:
|
||||
return f"{bytes_val:.2f}{unit}"
|
||||
bytes_val /= 1024.0
|
||||
return f"{bytes_val:.2f}PB"
|
||||
|
||||
def _handle_input(self, key: int) -> bool:
|
||||
"""Handle keyboard input. Returns False to exit."""
|
||||
if key == ord("q") or key == ord("Q"):
|
||||
return False # Exit
|
||||
|
||||
if self.current_screen == "selection":
|
||||
if key == curses.KEY_UP:
|
||||
self.selected_index = max(0, self.selected_index - 1)
|
||||
elif key == curses.KEY_DOWN:
|
||||
cgroups = self.collector.get_all_cgroups()
|
||||
self.selected_index = min(len(cgroups) - 1, self.selected_index + 1)
|
||||
elif key == ord("\n") or key == curses.KEY_ENTER or key == 10:
|
||||
# Select cgroup
|
||||
cgroups = self.collector.get_all_cgroups()
|
||||
if cgroups and 0 <= self.selected_index < len(cgroups):
|
||||
cgroups.sort(key=lambda c: c.name)
|
||||
self.selected_cgroup = cgroups[self.selected_index].id
|
||||
self.current_screen = "monitoring"
|
||||
elif key == ord("r") or key == ord("R"):
|
||||
# Force refresh cache
|
||||
self.collector._cgroup_cache_time = 0
|
||||
|
||||
elif self.current_screen == "monitoring":
|
||||
if key == 27 or key == ord("b") or key == ord("B"): # ESC or 'b'
|
||||
self.current_screen = "selection"
|
||||
self.selected_cgroup = None
|
||||
|
||||
return True # Continue running
|
||||
@ -16,6 +16,7 @@ from .helpers import (
|
||||
smp_processor_id,
|
||||
uid,
|
||||
skb_store_bytes,
|
||||
get_current_cgroup_id,
|
||||
get_stack,
|
||||
XDP_DROP,
|
||||
XDP_PASS,
|
||||
@ -79,6 +80,7 @@ __all__ = [
|
||||
"handle_helper_call",
|
||||
"emit_probe_read_kernel_str_call",
|
||||
"emit_probe_read_kernel_call",
|
||||
"get_current_cgroup_id",
|
||||
"ktime",
|
||||
"pid",
|
||||
"deref",
|
||||
|
||||
@ -30,6 +30,7 @@ class BPFHelperID(Enum):
|
||||
BPF_SKB_STORE_BYTES = 9
|
||||
BPF_GET_CURRENT_PID_TGID = 14
|
||||
BPF_GET_CURRENT_UID_GID = 15
|
||||
BPF_GET_CURRENT_CGROUP_ID = 80
|
||||
BPF_GET_CURRENT_COMM = 16
|
||||
BPF_PERF_EVENT_OUTPUT = 25
|
||||
BPF_GET_STACK = 67
|
||||
@ -68,6 +69,33 @@ def bpf_ktime_get_ns_emitter(
|
||||
return result, ir.IntType(64)
|
||||
|
||||
|
||||
@HelperHandlerRegistry.register(
|
||||
"get_current_cgroup_id",
|
||||
param_types=[],
|
||||
return_type=ir.IntType(64),
|
||||
)
|
||||
def bpf_get_current_cgroup_id(
|
||||
call,
|
||||
map_ptr,
|
||||
module,
|
||||
builder,
|
||||
func,
|
||||
local_sym_tab=None,
|
||||
struct_sym_tab=None,
|
||||
map_sym_tab=None,
|
||||
):
|
||||
"""
|
||||
Emit LLVM IR for bpf_get_current_cgroup_id helper function call.
|
||||
"""
|
||||
# func is an arg to just have a uniform signature with other emitters
|
||||
helper_id = ir.Constant(ir.IntType(64), BPFHelperID.BPF_GET_CURRENT_CGROUP_ID.value)
|
||||
fn_type = ir.FunctionType(ir.IntType(64), [], var_arg=False)
|
||||
fn_ptr_type = ir.PointerType(fn_type)
|
||||
fn_ptr = builder.inttoptr(helper_id, fn_ptr_type)
|
||||
result = builder.call(fn_ptr, [], tail=False)
|
||||
return result, ir.IntType(64)
|
||||
|
||||
|
||||
@HelperHandlerRegistry.register(
|
||||
"lookup",
|
||||
param_types=[ir.PointerType(ir.IntType(64))],
|
||||
|
||||
@ -57,6 +57,11 @@ def get_stack(buf, flags=0):
|
||||
return ctypes.c_int64(0)
|
||||
|
||||
|
||||
def get_current_cgroup_id():
|
||||
"""Get the current cgroup ID"""
|
||||
return ctypes.c_int64(0)
|
||||
|
||||
|
||||
XDP_ABORTED = ctypes.c_int64(0)
|
||||
XDP_DROP = ctypes.c_int64(1)
|
||||
XDP_PASS = ctypes.c_int64(2)
|
||||
|
||||
@ -1,19 +1,18 @@
|
||||
#include "vmlinux.h"
|
||||
#include <bpf/bpf_helpers.h>
|
||||
#include <linux/bpf.h>
|
||||
#include <linux/if_ether.h>
|
||||
#include <linux/ip.h>
|
||||
#include <bpf/bpf_helpers.h>
|
||||
|
||||
struct fake_iphdr {
|
||||
unsigned short useless;
|
||||
unsigned short tot_len;
|
||||
unsigned short id;
|
||||
unsigned short frag_off;
|
||||
unsigned char ttl;
|
||||
unsigned char protocol;
|
||||
unsigned short check;
|
||||
unsigned int saddr;
|
||||
unsigned int daddr;
|
||||
unsigned short useless;
|
||||
unsigned short tot_len;
|
||||
unsigned short id;
|
||||
unsigned short frag_off;
|
||||
unsigned char ttl;
|
||||
unsigned char protocol;
|
||||
unsigned short check;
|
||||
unsigned int saddr;
|
||||
unsigned int daddr;
|
||||
};
|
||||
|
||||
SEC("xdp")
|
||||
@ -21,16 +20,14 @@ int xdp_prog(struct xdp_md *ctx) {
|
||||
unsigned long data = ctx->data;
|
||||
unsigned long data_end = ctx->data_end;
|
||||
|
||||
if (data + sizeof(struct ethhdr) + sizeof(struct fake_iphdr) <= data_end) {
|
||||
struct fake_iphdr *iph = (void *)data + sizeof(struct ethhdr);
|
||||
|
||||
bpf_printk("%d", iph->saddr);
|
||||
|
||||
return XDP_PASS;
|
||||
} else {
|
||||
if (data + sizeof(struct ethhdr) + sizeof(struct fake_iphdr) > data_end) {
|
||||
return XDP_ABORTED;
|
||||
}
|
||||
struct task_struct * a = btf_bpf_get_current_task_btf();
|
||||
struct fake_iphdr *iph = (void *)data + sizeof(struct ethhdr);
|
||||
|
||||
bpf_printk("%d", iph->saddr);
|
||||
|
||||
return XDP_PASS;
|
||||
}
|
||||
|
||||
char _license[] SEC("license") = "GPL";
|
||||
|
||||
Reference in New Issue
Block a user