Skip to content

Commit

Permalink
feat(tui): display individual tracing flows in Tui (#777) - WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
fujiapple852 committed Nov 14, 2023
1 parent fd5ffdd commit 2d3993e
Show file tree
Hide file tree
Showing 16 changed files with 227 additions and 33 deletions.
23 changes: 22 additions & 1 deletion src/backend/trace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,13 @@ use trippy::tracing::{Probe, ProbeStatus, TracerRound};
#[derive(Debug, Clone)]
pub struct Trace {
max_samples: usize,
/// The flow id for the current round.
round_flow_id: FlowId,
/// Tracing data per registered flow id.
trace_data: HashMap<FlowId, TraceData>,
/// Flow registry.
registry: FlowRegistry,
/// Tracing error message.
error: Option<String>,
}

Expand All @@ -22,6 +27,7 @@ impl Trace {
Self {
trace_data: once((Self::default_flow_id(), TraceData::new(max_samples)))
.collect::<HashMap<FlowId, TraceData>>(),
round_flow_id: Self::default_flow_id(),
max_samples,
registry: FlowRegistry::new(),
error: None,
Expand Down Expand Up @@ -68,6 +74,11 @@ impl Trace {
self.trace_data[&flow_id].round_count()
}

/// The `FlowId` for the current round.
pub fn round_flow_id(&self) -> FlowId {
self.round_flow_id
}

/// The registry of flows in the trace.
pub fn flows(&self) -> &[(Flow, FlowId)] {
self.registry.flows()
Expand All @@ -94,6 +105,7 @@ impl Trace {
.map(|p| p.host),
);
let flow_id = self.registry.register(flow);
self.round_flow_id = flow_id;
self.update_trace_flow(Self::default_flow_id(), round);
self.update_trace_flow(flow_id, round);
}
Expand All @@ -110,17 +122,26 @@ impl Trace {
/// Information about a single `Hop` within a `Trace`.
#[derive(Debug, Clone)]
pub struct Hop {
/// The ttl of this hop.
ttl: u8,
/// The addrs of this hop and associated counts.
addrs: IndexMap<IpAddr, usize>,
/// The total probes sent for this hop.
total_sent: usize,
/// The total probes received for this hop.
total_recv: usize,
/// The total round trip time for this hop across all rounds.
total_time: Duration,
/// The round trip time for this hop in the current round.
last: Option<Duration>,
/// The best round trip time for this hop across all rounds.
best: Option<Duration>,
/// The worst round trip time for this hop across all rounds.
worst: Option<Duration>,
/// The history of round trip times across the last N rounds.
samples: Vec<Duration>,
mean: f64,
m2: f64,
samples: Vec<Duration>,
}

impl Hop {
Expand Down
8 changes: 8 additions & 0 deletions src/config/binding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub struct TuiBindings {
pub toggle_freeze: TuiKeyBinding,
pub toggle_chart: TuiKeyBinding,
pub toggle_map: TuiKeyBinding,
pub toggle_flows: TuiKeyBinding,
pub expand_hosts: TuiKeyBinding,
pub contract_hosts: TuiKeyBinding,
pub expand_hosts_max: TuiKeyBinding,
Expand Down Expand Up @@ -63,6 +64,7 @@ impl TuiBindings {
(self.toggle_freeze, TuiCommandItem::ToggleFreeze),
(self.toggle_chart, TuiCommandItem::ToggleChart),
(self.toggle_map, TuiCommandItem::ToggleMap),
(self.toggle_flows, TuiCommandItem::ToggleFlows),
(self.expand_hosts, TuiCommandItem::ExpandHosts),
(self.expand_hosts_max, TuiCommandItem::ExpandHostsMax),
(self.contract_hosts, TuiCommandItem::ContractHosts),
Expand Down Expand Up @@ -161,6 +163,10 @@ impl From<(HashMap<TuiCommandItem, TuiKeyBinding>, ConfigBindings)> for TuiBindi
.get(&TuiCommandItem::ToggleChart)
.or(cfg.toggle_chart.as_ref())
.unwrap_or(&TuiKeyBinding::new(KeyCode::Char('c'))),
toggle_flows: *cmd_items
.get(&TuiCommandItem::ToggleFlows)
.or(cfg.toggle_flows.as_ref())
.unwrap_or(&TuiKeyBinding::new(KeyCode::Char('f'))),
toggle_map: *cmd_items
.get(&TuiCommandItem::ToggleMap)
.or(cfg.toggle_map.as_ref())
Expand Down Expand Up @@ -472,6 +478,8 @@ pub enum TuiCommandItem {
ToggleChart,
/// Toggle the map.
ToggleMap,
/// Toggle the flows panel.
ToggleFlows,
/// Expand hosts.
ExpandHosts,
/// Expand hosts to max.
Expand Down
1 change: 1 addition & 0 deletions src/config/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ pub struct ConfigBindings {
pub address_mode_both: Option<TuiKeyBinding>,
pub toggle_freeze: Option<TuiKeyBinding>,
pub toggle_chart: Option<TuiKeyBinding>,
pub toggle_flows: Option<TuiKeyBinding>,
pub toggle_map: Option<TuiKeyBinding>,
pub expand_hosts: Option<TuiKeyBinding>,
pub contract_hosts: Option<TuiKeyBinding>,
Expand Down
20 changes: 16 additions & 4 deletions src/frontend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ pub fn run_frontend(
Ok(())
}

#[allow(clippy::too_many_lines)]
fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
trace_info: Vec<TraceInfo>,
Expand All @@ -60,6 +61,7 @@ fn run_app<B: Backend>(
if app.frozen_start.is_none() {
app.snapshot_trace_data();
app.clamp_selected_hop();
app.update_order_flow_counts();
};
terminal.draw(|f| render::app::render(f, &mut app))?;
if event::poll(app.tui_config.refresh_rate)? {
Expand Down Expand Up @@ -99,11 +101,19 @@ fn run_app<B: Backend>(
} else if bindings.previous_hop.check(key) {
app.previous_hop();
} else if bindings.previous_trace.check(key) {
app.previous_trace();
app.clear();
if app.show_flows {
app.previous_flow();
} else {
app.previous_trace();
app.clear();
}
} else if bindings.next_trace.check(key) {
app.next_trace();
app.clear();
if app.show_flows {
app.next_flow();
} else {
app.next_trace();
app.clear();
}
} else if bindings.next_hop_address.check(key) {
app.next_hop_address();
} else if bindings.previous_hop_address.check(key) {
Expand All @@ -120,6 +130,8 @@ fn run_app<B: Backend>(
app.toggle_chart();
} else if bindings.toggle_map.check(key) {
app.toggle_map();
} else if bindings.toggle_flows.check(key) {
app.toggle_flows();
} else if bindings.contract_hosts_min.check(key) {
app.contract_hosts_min();
} else if bindings.expand_hosts_max.check(key) {
Expand Down
2 changes: 2 additions & 0 deletions src/frontend/binding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub struct Bindings {
pub toggle_freeze: KeyBinding,
pub toggle_chart: KeyBinding,
pub toggle_map: KeyBinding,
pub toggle_flows: KeyBinding,
pub expand_hosts: KeyBinding,
pub contract_hosts: KeyBinding,
pub expand_hosts_max: KeyBinding,
Expand Down Expand Up @@ -53,6 +54,7 @@ impl From<TuiBindings> for Bindings {
toggle_freeze: KeyBinding::from(value.toggle_freeze),
toggle_chart: KeyBinding::from(value.toggle_chart),
toggle_map: KeyBinding::from(value.toggle_map),
toggle_flows: KeyBinding::from(value.toggle_flows),
expand_hosts: KeyBinding::from(value.expand_hosts),
contract_hosts: KeyBinding::from(value.contract_hosts),
expand_hosts_max: KeyBinding::from(value.expand_hosts_max),
Expand Down
1 change: 1 addition & 0 deletions src/frontend/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ pub mod app;
pub mod body;
pub mod bsod;
pub mod chart;
pub mod flows;
pub mod footer;
pub mod header;
pub mod help;
Expand Down
18 changes: 16 additions & 2 deletions src/frontend/render/app.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::frontend::render::{body, footer, header, help, settings, tabs};
use crate::frontend::render::{body, flows, footer, header, help, settings, tabs};
use crate::frontend::tui_app::TuiApp;
use ratatui::layout::{Constraint, Direction, Layout};
use ratatui::Frame;
Expand Down Expand Up @@ -33,7 +33,10 @@ use ratatui::Frame;
/// On startup a splash screen is shown in place of the hops table, until the completion of the
/// first round.
pub fn render(f: &mut Frame<'_>, app: &mut TuiApp) {
let constraints = if app.trace_info.len() > 1 {
// TODO ensure we don't allow flows and tabs at the same time or mix up layouts
let constraints = if app.show_flows {
LAYOUT_WITH_FLOWS.as_slice()
} else if app.trace_info.len() > 1 {
LAYOUT_WITH_TABS.as_slice()
} else {
LAYOUT_WITHOUT_TABS.as_slice()
Expand All @@ -47,6 +50,10 @@ pub fn render(f: &mut Frame<'_>, app: &mut TuiApp) {
tabs::render(f, chunks[1], app);
body::render(f, chunks[2], app);
footer::render(f, chunks[3], app);
} else if app.show_flows {
flows::render(f, chunks[1], app);
body::render(f, chunks[2], app);
footer::render(f, chunks[3], app);
} else {
body::render(f, chunks[1], app);
footer::render(f, chunks[2], app);
Expand All @@ -70,3 +77,10 @@ const LAYOUT_WITH_TABS: [Constraint; 4] = [
Constraint::Min(10),
Constraint::Length(6),
];

const LAYOUT_WITH_FLOWS: [Constraint; 4] = [
Constraint::Length(5),
Constraint::Length(6),
Constraint::Min(10),
Constraint::Length(6),
];
3 changes: 1 addition & 2 deletions src/frontend/render/chart.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use crate::backend::trace::Trace;
use crate::frontend::tui_app::TuiApp;
use ratatui::layout::{Alignment, Constraint, Rect};
use ratatui::style::Style;
Expand All @@ -13,7 +12,7 @@ pub fn render(f: &mut Frame<'_>, app: &TuiApp, rect: Rect) {
let samples = app.tui_config.max_samples / app.zoom_factor;
let series_data = app
.selected_tracer_data
.hops(Trace::default_flow_id())
.hops(app.selected_flow)
.iter()
.map(|hop| {
hop.samples()
Expand Down
58 changes: 58 additions & 0 deletions src/frontend/render/flows.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
use crate::frontend::tui_app::TuiApp;
use ratatui::layout::{Alignment, Rect};
use ratatui::prelude::Color;
use ratatui::style::{Modifier, Style};
use ratatui::text::Line;
use ratatui::widgets::{Bar, BarChart, BarGroup, Block, BorderType, Borders};
use ratatui::Frame;

/// Render the flows.
pub fn render(f: &mut Frame<'_>, rect: Rect, app: &TuiApp) {
let round_flow_id = app.tracer_data().round_flow_id();
let data: Vec<_> = app
.flow_counts
.iter()
.map(|(flow_id, count)| {
let (bar_fg, bg, fg) = if flow_id == &app.selected_flow && flow_id == &round_flow_id {
(
Color::Green,
app.tui_config.theme.frequency_chart_bar_color,
Color::Green,
)
} else if flow_id == &app.selected_flow {
(
Color::Green,
app.tui_config.theme.frequency_chart_bar_color,
app.tui_config.theme.frequency_chart_text_color,
)
} else if flow_id == &round_flow_id {
(Color::DarkGray, Color::DarkGray, Color::Green)
} else {
(Color::DarkGray, Color::DarkGray, Color::White)
};
Bar::default()
.label(Line::from(format!("{flow_id}")))
.value(*count as u64)
.style(Style::default().fg(bar_fg))
.value_style(Style::default().bg(bg).fg(fg).add_modifier(Modifier::BOLD))
})
.collect();
let block = Block::default()
.title("Flows")
.title_alignment(Alignment::Left)
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(app.tui_config.theme.border_color))
.style(
Style::default()
.bg(app.tui_config.theme.bg_color)
.fg(app.tui_config.theme.text_color),
);
let group = BarGroup::default().bars(&data);
let flow_counts = BarChart::default()
.block(block)
.data(group)
.bar_width(4)
.bar_gap(1);
f.render_widget(flow_counts, rect);
}
3 changes: 1 addition & 2 deletions src/frontend/render/header.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use crate::backend::trace::Trace;
use crate::frontend::tui_app::TuiApp;
use chrono::SecondsFormat;
use humantime::format_duration;
Expand Down Expand Up @@ -95,7 +94,7 @@ pub fn render(f: &mut Frame<'_>, app: &TuiApp, rect: Rect) {
Span::raw(render_status(app)),
Span::raw(format!(
", discovered {} hops",
app.tracer_data().hops(Trace::default_flow_id()).len()
app.tracer_data().hops(app.selected_flow).len()
)),
]),
];
Expand Down
7 changes: 4 additions & 3 deletions src/frontend/render/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,16 @@ pub fn render(f: &mut Frame<'_>, app: &TuiApp) {
f.render_widget(control, area);
}

const HELP_LINES: [&str; 20] = [
const HELP_LINES: [&str; 21] = [
"[up] & [down] - select hop",
"[left] & [right] - select trace",
"[left] & [right] - select trace or flow",
", & . - select hop address",
"[esc] - clear selection",
"d - toggle hop details",
"f - toggle flows",
"c - toggle chart",
"m - toggle map",
"ctrl+f - toggle freeze display",
"Ctrl-f - toggle freeze display",
"Ctrl+r - reset statistics",
"Ctrl+k - flush DNS cache",
"i - show IP only",
Expand Down
3 changes: 2 additions & 1 deletion src/frontend/render/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ fn format_binding_settings(app: &TuiApp) -> Vec<SettingsItem> {
SettingsItem::new("toggle-freeze", format!("{}", binds.toggle_freeze)),
SettingsItem::new("toggle-chart", format!("{}", binds.toggle_chart)),
SettingsItem::new("toggle-map", format!("{}", binds.toggle_map)),
SettingsItem::new("toggle-flows", format!("{}", binds.toggle_flows)),
SettingsItem::new("expand-hosts", format!("{}", binds.expand_hosts)),
SettingsItem::new("expand-hosts-max", format!("{}", binds.expand_hosts_max)),
SettingsItem::new("contract-hosts", format!("{}", binds.contract_hosts)),
Expand Down Expand Up @@ -403,7 +404,7 @@ pub const SETTINGS_TABS: [(&str, usize); 6] = [
("Trace", 14),
("Dns", 4),
("GeoIp", 1),
("Bindings", 26),
("Bindings", 27),
("Theme", 27),
];

Expand Down
15 changes: 7 additions & 8 deletions src/frontend/render/table.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::backend::trace::{Hop, Trace};
use crate::backend::trace::Hop;
use crate::config::{AddressMode, AsMode, GeoIpMode};
use crate::frontend::config::TuiConfig;
use crate::frontend::theme::Theme;
Expand Down Expand Up @@ -31,11 +31,10 @@ use trippy::dns::{AsInfo, DnsEntry, DnsResolver, Resolved, Resolver, Unresolved}
pub fn render(f: &mut Frame<'_>, app: &mut TuiApp, rect: Rect) {
let header = render_table_header(app.tui_config.theme);
let selected_style = Style::default().add_modifier(Modifier::REVERSED);
let rows = app
.tracer_data()
.hops(Trace::default_flow_id())
.iter()
.map(|hop| render_table_row(app, hop, &app.resolver, &app.geoip_lookup, &app.tui_config));
let rows =
app.tracer_data().hops(app.selected_flow).iter().map(|hop| {
render_table_row(app, hop, &app.resolver, &app.geoip_lookup, &app.tui_config)
});
let table = Table::new(rows)
.header(header)
.block(
Expand Down Expand Up @@ -78,8 +77,8 @@ fn render_table_row(
.selected_hop()
.map(|h| h.ttl() == hop.ttl())
.unwrap_or_default();
let is_target = app.tracer_data().is_target(hop, Trace::default_flow_id());
let is_in_round = app.tracer_data().is_in_round(hop, Trace::default_flow_id());
let is_target = app.tracer_data().is_target(hop, app.selected_flow);
let is_in_round = app.tracer_data().is_in_round(hop, app.selected_flow);
let ttl_cell = render_ttl_cell(hop);
let (hostname_cell, row_height) = if is_selected_hop && app.show_hop_details {
render_hostname_with_details(app, hop, dns, geoip_lookup, config)
Expand Down
4 changes: 2 additions & 2 deletions src/frontend/render/world.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::backend::trace::{Hop, Trace};
use crate::backend::trace::Hop;
use crate::frontend::tui_app::TuiApp;
use itertools::Itertools;
use ratatui::layout::{Alignment, Constraint, Direction, Layout, Margin, Rect};
Expand Down Expand Up @@ -186,7 +186,7 @@ struct MapEntry {
/// Each entry represent a single `GeoIp` location, which may be associated with multiple hops.
fn build_map_entries(app: &TuiApp) -> Vec<MapEntry> {
let mut geo_map: HashMap<String, MapEntry> = HashMap::new();
for hop in app.tracer_data().hops(Trace::default_flow_id()) {
for hop in app.tracer_data().hops(app.selected_flow) {
for addr in hop.addrs() {
if let Some(geo) = app.geoip_lookup.lookup(*addr).unwrap_or_default() {
if let Some((latitude, longitude, radius)) = geo.coordinates() {
Expand Down
Loading

0 comments on commit 2d3993e

Please sign in to comment.