Skip to content

Commit d73c793

Browse files
format chore
1 parent bbe4990 commit d73c793

File tree

3 files changed

+98
-64
lines changed

3 files changed

+98
-64
lines changed

BCC-Examples/container-monitor/container_monitor.py

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,18 @@
11
"""Container Monitor - TUI-based cgroup monitoring combining syscall, file I/O, and network tracking."""
22

3-
import time
4-
import os
5-
from pathlib import Path
63
from pythonbpf import bpf, map, section, bpfglobal, struct, BPF
74
from pythonbpf.maps import HashMap
85
from pythonbpf.helper import get_current_cgroup_id
96
from ctypes import c_int32, c_uint64, c_void_p
107
from vmlinux import struct_pt_regs, struct_sk_buff
118

12-
from data_collector import ContainerDataCollector
9+
from data_collection import ContainerDataCollector
1310
from tui import ContainerMonitorTUI
1411

1512

1613
# ==================== BPF Structs ====================
1714

15+
1816
@bpf
1917
@struct
2018
class read_stats:
@@ -40,6 +38,7 @@ class net_stats:
4038

4139
# ==================== BPF Maps ====================
4240

41+
4342
@bpf
4443
@map
4544
def read_map() -> HashMap:
@@ -66,6 +65,7 @@ def syscall_count() -> HashMap:
6665

6766
# ==================== File I/O Tracing ====================
6867

68+
6969
@bpf
7070
@section("kprobe/vfs_read")
7171
def trace_read(ctx: struct_pt_regs) -> c_int32:
@@ -109,6 +109,7 @@ def trace_write(ctx1: struct_pt_regs) -> c_int32:
109109

110110
# ==================== Network I/O Tracing ====================
111111

112+
112113
@bpf
113114
@section("kprobe/__netif_receive_skb")
114115
def trace_netif_rx(ctx2: struct_pt_regs) -> c_int32:
@@ -165,6 +166,7 @@ def trace_dev_xmit(ctx3: struct_pt_regs) -> c_int32:
165166

166167
# ==================== Syscall Tracing ====================
167168

169+
168170
@bpf
169171
@section("tracepoint/raw_syscalls/sys_enter")
170172
def count_syscalls(ctx: c_void_p) -> c_int32:
@@ -190,32 +192,29 @@ def LICENSE() -> str:
190192

191193
if __name__ == "__main__":
192194
print("🔥 Loading BPF programs...")
193-
195+
194196
# Load and attach BPF program
195197
b = BPF()
196198
b.load()
197199
b.attach_all()
198-
200+
199201
# Get map references and enable struct deserialization
200202
read_map_ref = b["read_map"]
201203
write_map_ref = b["write_map"]
202204
net_stats_map_ref = b["net_stats_map"]
203205
syscall_count_ref = b["syscall_count"]
204-
206+
205207
read_map_ref.set_value_struct("read_stats")
206208
write_map_ref.set_value_struct("write_stats")
207209
net_stats_map_ref.set_value_struct("net_stats")
208-
210+
209211
print("✅ BPF programs loaded and attached")
210-
212+
211213
# Setup data collector
212214
collector = ContainerDataCollector(
213-
read_map_ref,
214-
write_map_ref,
215-
net_stats_map_ref,
216-
syscall_count_ref
215+
read_map_ref, write_map_ref, net_stats_map_ref, syscall_count_ref
217216
)
218-
217+
219218
# Create and run TUI
220219
tui = ContainerMonitorTUI(collector)
221220
tui.run()

BCC-Examples/container-monitor/data_collector.py renamed to BCC-Examples/container-monitor/data_collection.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33
import os
44
import time
55
from pathlib import Path
6-
from typing import Dict, List, Set, Optional, Tuple
6+
from typing import Dict, List, Set, Optional
77
from dataclasses import dataclass
88
from collections import deque, defaultdict
99

1010

1111
@dataclass
1212
class CgroupInfo:
1313
"""Information about a cgroup."""
14+
1415
id: int
1516
name: str
1617
path: str
@@ -19,6 +20,7 @@ class CgroupInfo:
1920
@dataclass
2021
class ContainerStats:
2122
"""Statistics for a container/cgroup."""
23+
2224
cgroup_id: int
2325
cgroup_name: str
2426

@@ -44,7 +46,9 @@ class ContainerStats:
4446
class ContainerDataCollector:
4547
"""Collects and manages container monitoring data from BPF."""
4648

47-
def __init__(self, read_map, write_map, net_stats_map, syscall_map, history_size: int = 100):
49+
def __init__(
50+
self, read_map, write_map, net_stats_map, syscall_map, history_size: int = 100
51+
):
4852
self.read_map = read_map
4953
self.write_map = write_map
5054
self.net_stats_map = net_stats_map
@@ -53,12 +57,14 @@ def __init__(self, read_map, write_map, net_stats_map, syscall_map, history_size
5357
# Caching
5458
self._cgroup_cache: Dict[int, CgroupInfo] = {}
5559
self._cgroup_cache_time = 0
56-
self._cache_ttl = 5.
60+
self._cache_ttl = 5.0
5761
0 # Refresh cache every 5 seconds
5862

5963
# Historical data for graphing
6064
self._history_size = history_size
61-
self._history: Dict[int, deque] = defaultdict(lambda: deque(maxlen=history_size))
65+
self._history: Dict[int, deque] = defaultdict(
66+
lambda: deque(maxlen=history_size)
67+
)
6268

6369
def get_all_cgroups(self) -> List[CgroupInfo]:
6470
"""Get all cgroups with caching."""
@@ -105,11 +111,7 @@ def _refresh_cgroup_cache(self):
105111
best_path = self._get_best_cgroup_path(paths)
106112
name = self._get_cgroup_name(best_path)
107113

108-
new_cache[cgroup_id] = CgroupInfo(
109-
id=cgroup_id,
110-
name=name,
111-
path=best_path
112-
)
114+
new_cache[cgroup_id] = CgroupInfo(id=cgroup_id, name=name, path=best_path)
113115

114116
self._cgroup_cache = new_cache
115117
self._cgroup_cache_time = time.time()
@@ -120,13 +122,13 @@ def _get_best_cgroup_path(self, paths: Set[str]) -> str:
120122

121123
# Prefer paths with more components (more specific)
122124
# Prefer paths containing docker, podman, etc.
123-
for keyword in ['docker', 'podman', 'kubernetes', 'k8s', 'systemd']:
125+
for keyword in ["docker", "podman", "kubernetes", "k8s", "systemd"]:
124126
for path in path_list:
125127
if keyword in path.lower():
126128
return path
127129

128130
# Return longest path (most specific)
129-
return max(path_list, key=lambda p: (len(p.split('/')), len(p)))
131+
return max(path_list, key=lambda p: (len(p.split("/")), len(p)))
130132

131133
def _get_cgroup_name(self, path: str) -> str:
132134
"""Extract a friendly name from cgroup path."""
@@ -165,9 +167,7 @@ def get_stats_for_cgroup(self, cgroup_id: int) -> ContainerStats:
165167
cgroup_name = cgroup_info.name if cgroup_info else f"cgroup-{cgroup_id}"
166168

167169
stats = ContainerStats(
168-
cgroup_id=cgroup_id,
169-
cgroup_name=cgroup_name,
170-
timestamp=time.time()
170+
cgroup_id=cgroup_id, cgroup_name=cgroup_name, timestamp=time.time()
171171
)
172172

173173
# Get file I/O stats

BCC-Examples/container-monitor/tui.py

Lines changed: 71 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
"""Terminal User Interface for container monitoring."""
22

3-
import sys
43
import time
54
import curses
65
from typing import Optional, List
7-
from data_collector import ContainerDataCollector, CgroupInfo, ContainerStats
6+
from data_collection import ContainerDataCollector
87

98

109
class ContainerMonitorTUI:
@@ -122,11 +121,11 @@ def _draw_selection_screen(self, stdscr):
122121
# Highlight selected
123122
stdscr.attron(curses.color_pair(2) | curses.A_BOLD | curses.A_REVERSE)
124123
line = f"► {cgroup.name:<40} ID: {cgroup.id}"
125-
stdscr.addstr(y, 2, line[:width - 4])
124+
stdscr.addstr(y, 2, line[: width - 4])
126125
stdscr.attroff(curses.color_pair(2) | curses.A_BOLD | curses.A_REVERSE)
127126
else:
128127
line = f" {cgroup.name:<40} ID: {cgroup.id}"
129-
stdscr.addstr(y, 2, line[:width - 4])
128+
stdscr.addstr(y, 2, line[: width - 4])
130129

131130
# Footer with count
132131
footer = f"Total cgroups: {len(cgroups)}"
@@ -174,43 +173,75 @@ def _draw_monitoring_screen(self, stdscr):
174173

175174
# RX graph
176175
y += 2
177-
rx_label = f"RX: {self._format_bytes(stats.rx_bytes)} ({stats.rx_packets:,} packets)"
176+
rx_label = (
177+
f"RX: {self._format_bytes(stats.rx_bytes)} ({stats.rx_packets:,} packets)"
178+
)
178179
stdscr.addstr(y, 2, rx_label)
179180
if len(history) > 1:
180-
self._draw_bar_graph(stdscr, y + 1, 2, width - 4, 3,
181-
[s.rx_bytes for s in history],
182-
curses.color_pair(2))
181+
self._draw_bar_graph(
182+
stdscr,
183+
y + 1,
184+
2,
185+
width - 4,
186+
3,
187+
[s.rx_bytes for s in history],
188+
curses.color_pair(2),
189+
)
183190

184191
# TX graph
185192
y += 5
186-
tx_label = f"TX: {self._format_bytes(stats.tx_bytes)} ({stats.tx_packets:,} packets)"
193+
tx_label = (
194+
f"TX: {self._format_bytes(stats.tx_bytes)} ({stats.tx_packets:,} packets)"
195+
)
187196
stdscr.addstr(y, 2, tx_label)
188197
if len(history) > 1:
189-
self._draw_bar_graph(stdscr, y + 1, 2, width - 4, 3,
190-
[s.tx_bytes for s in history],
191-
curses.color_pair(3))
198+
self._draw_bar_graph(
199+
stdscr,
200+
y + 1,
201+
2,
202+
width - 4,
203+
3,
204+
[s.tx_bytes for s in history],
205+
curses.color_pair(3),
206+
)
192207

193208
# File I/O graphs
194209
y += 5
195210
self._draw_section_header(stdscr, y, "FILE I/O", 1)
196211

197212
# Read graph
198213
y += 2
199-
read_label = f"READ: {self._format_bytes(stats.read_bytes)} ({stats.read_ops:,} ops)"
214+
read_label = (
215+
f"READ: {self._format_bytes(stats.read_bytes)} ({stats.read_ops:,} ops)"
216+
)
200217
stdscr.addstr(y, 2, read_label)
201218
if len(history) > 1:
202-
self._draw_bar_graph(stdscr, y + 1, 2, width - 4, 3,
203-
[s.read_bytes for s in history],
204-
curses.color_pair(4))
219+
self._draw_bar_graph(
220+
stdscr,
221+
y + 1,
222+
2,
223+
width - 4,
224+
3,
225+
[s.read_bytes for s in history],
226+
curses.color_pair(4),
227+
)
205228

206229
# Write graph
207230
y += 5
208-
write_label = f"WRITE: {self._format_bytes(stats.write_bytes)} ({stats.write_ops:,} ops)"
231+
write_label = (
232+
f"WRITE: {self._format_bytes(stats.write_bytes)} ({stats.write_ops:,} ops)"
233+
)
209234
stdscr.addstr(y, 2, write_label)
210235
if len(history) > 1:
211-
self._draw_bar_graph(stdscr, y + 1, 2, width - 4, 3,
212-
[s.write_bytes for s in history],
213-
curses.color_pair(5))
236+
self._draw_bar_graph(
237+
stdscr,
238+
y + 1,
239+
2,
240+
width - 4,
241+
3,
242+
[s.write_bytes for s in history],
243+
curses.color_pair(5),
244+
)
214245

215246
def _draw_section_header(self, stdscr, y: int, title: str, color_pair: int):
216247
"""Draw a section header."""
@@ -220,8 +251,16 @@ def _draw_section_header(self, stdscr, y: int, title: str, color_pair: int):
220251
stdscr.addstr(y, len(title) + 3, "─" * (width - len(title) - 5))
221252
stdscr.attroff(curses.color_pair(color_pair) | curses.A_BOLD)
222253

223-
def _draw_bar_graph(self, stdscr, y: int, x: int, width: int, height: int,
224-
data: List[float], color_pair: int):
254+
def _draw_bar_graph(
255+
self,
256+
stdscr,
257+
y: int,
258+
x: int,
259+
width: int,
260+
height: int,
261+
data: List[float],
262+
color_pair: int,
263+
):
225264
"""Draw a simple bar graph."""
226265
if not data or width < 2:
227266
return
@@ -250,25 +289,21 @@ def _draw_bar_graph(self, stdscr, y: int, x: int, width: int, height: int,
250289
else:
251290
bar_line += " "
252291

253-
try:
254-
stdscr.attron(color_pair)
255-
stdscr.addstr(y + row, x, bar_line[:width])
256-
stdscr.attroff(color_pair)
257-
except:
258-
pass # Ignore errors at screen edges
292+
stdscr.attron(color_pair)
293+
stdscr.addstr(y + row, x, bar_line[:width])
294+
stdscr.attroff(color_pair)
259295

260-
def _format_bytes(self, bytes_val: int) -> str:
296+
def _format_bytes(self, bytes_val: float) -> str:
261297
"""Format bytes into human-readable string."""
262-
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
298+
for unit in ["B", "KB", "MB", "GB", "TB"]:
263299
if bytes_val < 1024.0:
264300
return f"{bytes_val:.2f} {unit}"
265-
bytes_val /= 1024.
266-
0
301+
bytes_val /= 1024.0
267302
return f"{bytes_val:.2f} PB"
268303

269304
def _handle_input(self, key: int) -> bool:
270305
"""Handle keyboard input. Returns False to exit."""
271-
if key == ord('q') or key == ord('Q'):
306+
if key == ord("q") or key == ord("Q"):
272307
return False # Exit
273308

274309
if self.current_screen == "selection":
@@ -277,19 +312,19 @@ def _handle_input(self, key: int) -> bool:
277312
elif key == curses.KEY_DOWN:
278313
cgroups = self.collector.get_all_cgroups()
279314
self.selected_index = min(len(cgroups) - 1, self.selected_index + 1)
280-
elif key == ord('\n') or key == curses.KEY_ENTER or key == 10:
315+
elif key == ord("\n") or key == curses.KEY_ENTER or key == 10:
281316
# Select cgroup
282317
cgroups = self.collector.get_all_cgroups()
283318
if cgroups and 0 <= self.selected_index < len(cgroups):
284319
cgroups.sort(key=lambda c: c.name)
285320
self.selected_cgroup = cgroups[self.selected_index].id
286321
self.current_screen = "monitoring"
287-
elif key == ord('r') or key == ord('R'):
322+
elif key == ord("r") or key == ord("R"):
288323
# Force refresh cache
289324
self.collector._cgroup_cache_time = 0
290325

291326
elif self.current_screen == "monitoring":
292-
if key == 27 or key == ord('b') or key == ord('B'): # ESC or 'b'
327+
if key == 27 or key == ord("b") or key == ord("B"): # ESC or 'b'
293328
self.current_screen = "selection"
294329
self.selected_cgroup = None
295330

0 commit comments

Comments
 (0)