-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(gazer): created TUI to visualize scraped data
- Loading branch information
Showing
4 changed files
with
171 additions
and
138 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import threading | ||
import time | ||
import termplotlib as tpl | ||
import pandas as pd | ||
from bcc import BPF | ||
from socket import inet_ntop, AF_INET | ||
from struct import pack | ||
|
||
|
||
class Gazer: | ||
request_df = pd.DataFrame(columns=["PID", "COMM", "LADDR", "LPORT", "RADDR", "RPORT", "TX_KB", "RX_KB", "MS"]) | ||
syn_df = pd.DataFrame(columns=["backlog", "slot", "saddr", "lport", "value", "outdated"]) | ||
bpf_text = "" | ||
|
||
def __init__(self): | ||
with open('bpf.c', 'r') as f: | ||
bpf_text = f.read() | ||
|
||
with open('sock_state.c', 'r') as f: | ||
bpf_text += f.read() | ||
|
||
with open('syn_backlog.c', 'r') as f: | ||
bpf_text += f.read() | ||
|
||
bpf_text = bpf_text.replace('FILTER_PID', '') | ||
self.bpf_text = bpf_text.replace('ADDRFILTER', '') | ||
self.b = BPF(text=self.bpf_text) | ||
self.b["ipv4_events"].open_perf_buffer(self.ipv4_request_event, page_cnt=64) | ||
self.syn_backlog_buffer = self.b['syn_backlog'] | ||
|
||
def ipv4_request_event(self, cpu, data, size): | ||
event = self.b["ipv4_events"].event(data) | ||
self.request_df = self.request_df.append({ | ||
"PID": event.pid, | ||
"COMM": event.task.decode('utf-8', 'replace'), | ||
"LADDR": inet_ntop(AF_INET, pack("I", event.saddr)), | ||
"LPORT": event.ports >> 32, | ||
"RADDR": inet_ntop(AF_INET, pack("I", event.daddr)), | ||
"RPORT": event.ports & 0xffffffff, | ||
"TX_KB": event.tx_b, | ||
"RX_KB": event.rx_b, | ||
"MS": float(event.span_us) / 1000, | ||
}, ignore_index=True) | ||
self.request_df = self.request_df[-10:] | ||
|
||
def poll_requests(self): | ||
while True: | ||
self.b.perf_buffer_poll() | ||
|
||
def poll_syn_backlog(self): | ||
while True: | ||
data = self.syn_backlog_buffer.items() | ||
self.syn_df["outdated"] = True | ||
for row in data: | ||
self.syn_df = self.syn_df.append({ | ||
"backlog": row[0].backlog, | ||
"slot": row[0].slot, | ||
"saddr": inet_ntop(AF_INET, pack("I", row[0].saddr)), | ||
"lport": row[0].lport, | ||
"value": row[1].value, | ||
"outdated": False, | ||
}, ignore_index=True) | ||
self.syn_backlog_buffer.clear() | ||
time.sleep(5) | ||
|
||
def syn_backlog_text(self): | ||
self.syn_df['saddr_port'] = self.syn_df["saddr"] + ":" + self.syn_df["lport"].astype(str) | ||
grouped_df = self.syn_df.groupby('saddr_port') | ||
out = "SYN Backlog\n" | ||
for key, item in grouped_df: | ||
x, y = [], [] | ||
out += f"\n{key}\n" | ||
df = grouped_df.get_group(key) | ||
new_data = df.loc[df['outdated'] == False] | ||
|
||
if new_data.empty: | ||
x.append(0) | ||
y.append("0-1") | ||
else: | ||
for entry in new_data.sort_values(by=['slot']).to_dict('records'): | ||
x.append(entry['value']) | ||
y.append(f"{entry['slot'] - 1} -> {entry['slot']}") | ||
fig = tpl.figure() | ||
fig.barh(x, y, force_ascii=True) | ||
out += fig.get_string() + "\n" | ||
return out | ||
|
||
def request_log_text(self): | ||
if self.request_df.empty: | ||
return "" | ||
return self.request_df.tail(10).__str__() | ||
|
||
def poll_data_in_bg(self): | ||
poll_syn_backlog = threading.Thread(target=self.poll_syn_backlog, args=()) | ||
poll_syn_backlog.daemon = True | ||
poll_syn_backlog.start() | ||
|
||
poll_requests = threading.Thread(target=self.poll_requests, args=()) | ||
poll_requests.daemon = True | ||
poll_requests.start() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,141 +1,70 @@ | ||
#!/usr/bin/python | ||
# @lint-avoid-python-3-compatibility-imports | ||
# | ||
# tcplife Trace the lifespan of TCP sessions and summarize. | ||
# For Linux, uses BCC, BPF. Embedded C. | ||
# | ||
# USAGE: tcplife [-h] [-C] [-S] [-p PID] [-4 | -6] [interval [count]] | ||
# | ||
# This uses the sock:inet_sock_set_state tracepoint if it exists (added to | ||
# Linux 4.16, and replacing the earlier tcp:tcp_set_state), else it uses | ||
# kernel dynamic tracing of tcp_set_state(). | ||
# | ||
# While throughput counters are emitted, they are fetched in a low-overhead | ||
# manner: reading members of the tcp_info struct on TCP close. ie, we do not | ||
# trace send/receive. | ||
# | ||
# Copyright 2016 Netflix, Inc. | ||
# Licensed under the Apache License, Version 2.0 (the "License") | ||
# | ||
# IDEA: Julia Evans | ||
# | ||
# 18-Oct-2016 Brendan Gregg Created this. | ||
# 29-Dec-2017 " " Added tracepoint support. | ||
|
||
from __future__ import print_function | ||
|
||
import curses | ||
import time | ||
from gazer import Gazer | ||
|
||
|
||
def draw_menu(stdscr: curses.window): | ||
k = 0 | ||
cursor_x = 0 | ||
cursor_y = 0 | ||
|
||
# Clear and refresh the screen for a blank canvas | ||
stdscr.clear() | ||
stdscr.refresh() | ||
stdscr.nodelay(True) | ||
# curses.delay_output(100) | ||
|
||
# Start colors in curses | ||
curses.start_color() | ||
curses.init_pair(1, curses.COLOR_CYAN, curses.COLOR_BLACK) | ||
curses.init_pair(2, curses.COLOR_RED, curses.COLOR_BLACK) | ||
curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_WHITE) | ||
|
||
# Loop where k is the last character pressed | ||
while k != ord('q'): | ||
|
||
# Initialization | ||
stdscr.clear() | ||
height, width = stdscr.getmaxyx() | ||
|
||
if k == curses.KEY_DOWN: | ||
cursor_y = cursor_y + 1 | ||
elif k == curses.KEY_UP: | ||
cursor_y = cursor_y - 1 | ||
elif k == curses.KEY_RIGHT: | ||
cursor_x = cursor_x + 1 | ||
elif k == curses.KEY_LEFT: | ||
cursor_x = cursor_x - 1 | ||
|
||
cursor_x = max(0, cursor_x) | ||
cursor_x = min(width - 1, cursor_x) | ||
|
||
cursor_y = max(0, cursor_y) | ||
cursor_y = min(height - 1, cursor_y) | ||
|
||
statusbarstr = "Press 'q' to exit | STATUS BAR | Pos: {}, {}".format(cursor_x, cursor_y) | ||
|
||
stdscr.addstr(0, 0, "Requests", curses.color_pair(1)) | ||
stdscr.addstr(2, 0, gazer.request_log_text(), curses.color_pair(1)) | ||
stdscr.addstr(15, 0, gazer.syn_backlog_text(), curses.color_pair(1)) | ||
|
||
# Render status bar | ||
stdscr.attron(curses.color_pair(3)) | ||
stdscr.addstr(height - 1, 0, statusbarstr) | ||
stdscr.addstr(height - 1, len(statusbarstr), " " * (width - len(statusbarstr) - 1)) | ||
stdscr.attroff(curses.color_pair(3)) | ||
|
||
stdscr.move(cursor_y, cursor_x) | ||
|
||
# Refresh the screen | ||
stdscr.refresh() | ||
|
||
# Wait for next input | ||
k = stdscr.getch() | ||
time.sleep(1 / 30) | ||
|
||
from bcc import BPF | ||
import argparse | ||
from socket import inet_ntop, AF_INET, inet_aton | ||
from struct import pack, unpack | ||
|
||
# arguments | ||
examples = """examples: | ||
./tcplife # trace all TCP connect()s | ||
./tcplife -T # include time column (HH:MM:SS) | ||
./tcplife -w # wider columns (fit IPv6) | ||
./tcplife -stT # csv output, with times & timestamps | ||
./tcplife -p 181 # only trace PID 181 | ||
./tcplife -L 80 # only trace local port 80 | ||
./tcplife -L 80,81 # only trace local ports 80 and 81 | ||
./tcplife -D 80 # only trace remote port 80 | ||
./tcplife -4 # only trace IPv4 family | ||
./tcplife -6 # only trace IPv6 family | ||
""" | ||
parser = argparse.ArgumentParser( | ||
description="Trace the lifespan of TCP sessions and summarize", | ||
formatter_class=argparse.RawDescriptionHelpFormatter, | ||
epilog=examples) | ||
parser.add_argument("-w", "--wide", action="store_true", | ||
help="wide column output (fits IPv6 addresses)") | ||
parser.add_argument("-p", "--pid", | ||
help="trace this PID only") | ||
parser.add_argument("-a", "--addr", | ||
help="filter for address") | ||
parser.add_argument("--ebpf", action="store_true", | ||
help=argparse.SUPPRESS) | ||
args = parser.parse_args() | ||
debug = 0 | ||
|
||
# define BPF program | ||
with open('bpf.c', 'r') as f: | ||
bpf_text = f.read() | ||
|
||
with open('sock_state.c', 'r') as f: | ||
bpf_text += f.read() | ||
|
||
with open('syn_backlog.c', 'r') as f: | ||
bpf_text += f.read() | ||
|
||
# code substitutions | ||
if args.pid: | ||
bpf_text = bpf_text.replace('FILTER_PID', | ||
'if (pid != %s) { return 0; }' % args.pid) | ||
|
||
if args.addr: | ||
bpf_text = bpf_text.replace('ADDRFILTER', | ||
"""if (data4.saddr != {0} || data4.daddr != {0}) | ||
return 0;""".format(unpack("=I", inet_aton(args.addr))[0])) | ||
|
||
bpf_text = bpf_text.replace('FILTER_PID', '') | ||
bpf_text = bpf_text.replace('ADDRFILTER', '') | ||
|
||
# | ||
# Setup output formats | ||
# | ||
# Don't change the default output (next 2 lines): this fits in 80 chars. I | ||
# know it doesn't have NS or UIDs etc. I know. If you really, really, really | ||
# need to add columns, columns that solve real actual problems, I'd start by | ||
# adding an extended mode (-x) to included those columns. | ||
# | ||
header_string = "%-5s %-10.10s %s%-15s %-5s %-15s %-5s %5s %5s %s" | ||
format_string = "%-5d %-10.10s %s%-15s %-5d %-15s %-5d %5d %5d %.2f" | ||
|
||
|
||
# process event | ||
def print_ipv4_event(cpu, data, size): | ||
event = b["ipv4_events"].event(data) | ||
print(format_string % (event.pid, event.task.decode('utf-8', 'replace'), | ||
"", | ||
inet_ntop(AF_INET, pack("I", event.saddr)), event.ports >> 32, | ||
inet_ntop(AF_INET, pack("I", event.daddr)), event.ports & 0xffffffff, | ||
event.tx_b, event.rx_b, float(event.span_us) / 1000)) | ||
|
||
|
||
# initialize BPF | ||
b = BPF(text=bpf_text) | ||
|
||
# header | ||
print(header_string % ("PID", "COMM", "", "LADDR", | ||
"LPORT", "RADDR", "RPORT", "TX_KB", "RX_KB", "MS")) | ||
|
||
start_ts = 0 | ||
|
||
# read events | ||
b["ipv4_events"].open_perf_buffer(print_ipv4_event, page_cnt=64) | ||
# b.attach_kprobe(event="tcp_v4_syn_recv_sock", fn_name="update_syn_backlog") | ||
while 1: | ||
try: | ||
b.perf_buffer_poll() | ||
except KeyboardInterrupt: | ||
# exit() | ||
break | ||
|
||
# while True: | ||
# try: | ||
# time.sleep(999999) | ||
# except KeyboardInterrupt: | ||
# break | ||
|
||
dist = b['syn_backlog'] | ||
print() | ||
for item in dist.items(): | ||
print({"backlog": item[0].backlog, | ||
"slot": item[0].slot, | ||
"saddr": inet_ntop(AF_INET, pack("I", item[0].saddr)), | ||
"lport": item[0].lport, | ||
"value": item[1].value}) | ||
|
||
gazer = Gazer() | ||
gazer.poll_data_in_bg() | ||
|
||
curses.wrapper(draw_menu) |