use std::collections::BTreeMap;
use std::time::{Duration, Instant};
use ratatui::Terminal;
use ratatui::backend::TestBackend;
use ratatui::layout::Rect;
use sozu_command_lib::proto::command::{
AggregatedMetrics, BackendMetrics, ClusterMetrics, FilteredMetrics, ListenersList, Percentiles,
filtered_metrics,
};
use sozu_lib::metrics::names;
use super::app::App;
use super::panes;
use super::theme::Skin;
use super::transport::{ListenersSnapshot, Snapshot};
fn render_to_string<F>(width: u16, height: u16, app: &App, draw: F) -> String
where
F: FnOnce(&mut ratatui::Frame<'_>, Rect, &App, &Skin),
{
let backend = TestBackend::new(width, height);
let mut terminal = Terminal::new(backend).expect("TestBackend Terminal");
let skin = Skin::default_dark();
terminal
.draw(|f| draw(f, f.area(), app, &skin))
.expect("draw");
let buffer = terminal.backend().buffer().clone();
let area = *buffer.area();
let mut out = String::with_capacity((area.width as usize + 1) * area.height as usize);
for y in 0..area.height {
for x in 0..area.width {
out.push_str(buffer[(x, y)].symbol());
}
while out.ends_with(' ') {
out.pop();
}
out.push('\n');
}
out
}
fn fixture_metrics_at(tick: u64) -> AggregatedMetrics {
let tick = tick as i64;
let mut proxying: BTreeMap<String, FilteredMetrics> = BTreeMap::new();
proxying.insert(names::slab::USAGE_PERCENT.into(), gauge(45));
proxying.insert(names::client::CONNECTIONS.into(), gauge(312));
proxying.insert(names::http::ACTIVE_REQUESTS.into(), gauge(87));
proxying.insert(names::h2::CONNECTION_ACTIVE_STREAMS.into(), gauge(24));
proxying.insert(names::http::ALPN_H2.into(), count(1_000 + 100 * tick));
proxying.insert(names::http::ALPN_HTTP11.into(), count(500 + 50 * tick));
proxying.insert(names::h2::CONNECTION_WINDOW_BYTES.into(), gauge(65_535));
proxying.insert(
names::h2::CONNECTION_PENDING_WINDOW_UPDATES.into(),
gauge(0),
);
proxying.insert(names::h2::FLOW_CONTROL_STALL.into(), count(2));
proxying.insert(names::h2::FRAMES_TX_WINDOW_UPDATE.into(), count(42));
proxying.insert(names::h2::FRAMES_TX_RST_STREAM.into(), count(0));
proxying.insert(names::h2::FRAMES_TX_GOAWAY.into(), count(0));
proxying.insert(names::h2::HEADERS_REJECTED_BUDGET_OVERRUN.into(), count(0));
proxying.insert(
names::event_loop::SERVICE_TIME.into(),
percentiles(3, 8, 12),
);
let mut clusters: BTreeMap<String, ClusterMetrics> = BTreeMap::new();
for (i, id) in ["api-prod", "static-cdn", "queue-worker"]
.iter()
.enumerate()
{
let mut cluster: BTreeMap<String, FilteredMetrics> = BTreeMap::new();
let i64_i = i as i64;
cluster.insert(
names::backend::REQUESTS.into(),
count((1_000 + i64_i * 500) * tick),
);
cluster.insert(names::http_status::S500.into(), count(2 + i64_i));
cluster.insert(names::http_status::S503.into(), count(1));
cluster.insert(
names::backend::RESPONSE_TIME.into(),
percentiles(20, 80, 180 + i as u64 * 20),
);
cluster.insert(names::cluster::TOTAL_BACKENDS.into(), gauge(2));
cluster.insert(
names::cluster::AVAILABLE_BACKENDS.into(),
gauge(if i == 2 { 0 } else { 2 }),
);
let tick_u64 = tick as u64;
let backends = vec![
backend_metrics(
format!("{id}-1"),
125_000 * (i as u64 + 1) * tick_u64,
30,
110,
),
backend_metrics(
format!("{id}-2"),
250_000 * (i as u64 + 1) * tick_u64,
35,
120,
),
];
clusters.insert((*id).into(), ClusterMetrics { cluster, backends });
}
AggregatedMetrics {
main: BTreeMap::new(),
workers: BTreeMap::new(),
clusters,
proxying,
}
}
fn fixture_listeners() -> ListenersList {
ListenersList {
http_listeners: BTreeMap::new(),
https_listeners: BTreeMap::new(),
tcp_listeners: BTreeMap::new(),
udp_listeners: BTreeMap::new(),
}
}
fn gauge(v: u64) -> FilteredMetrics {
FilteredMetrics {
inner: Some(filtered_metrics::Inner::Gauge(v)),
}
}
fn count(v: i64) -> FilteredMetrics {
FilteredMetrics {
inner: Some(filtered_metrics::Inner::Count(v)),
}
}
fn percentiles(p50: u64, p90: u64, p99: u64) -> FilteredMetrics {
FilteredMetrics {
inner: Some(filtered_metrics::Inner::Percentiles(Percentiles {
samples: 1_000,
p_50: p50,
p_90: p90,
p_99: p99,
p_99_9: p99 * 2,
p_99_99: p99 * 3,
p_99_999: p99 * 4,
p_100: p99 * 5,
sum: 1_000 * p50,
})),
}
}
fn backend_metrics(id: String, bytes: u64, p50: u64, p99: u64) -> BackendMetrics {
let mut metrics: BTreeMap<String, FilteredMetrics> = BTreeMap::new();
metrics.insert(names::backend::BYTES_IN.into(), count(bytes as i64));
metrics.insert(names::backend::BYTES_OUT.into(), count((bytes * 4) as i64));
metrics.insert(names::backend::CONNECTIONS_PER_BACKEND.into(), gauge(12));
metrics.insert(
names::backend::RESPONSE_TIME.into(),
percentiles(p50, p50 + 30, p99),
);
metrics.insert(names::backend::REQUESTS.into(), count(bytes as i64 / 4));
BackendMetrics {
backend_id: id,
metrics,
}
}
fn fixture_app() -> App {
let mut app = App::new();
let t0 = Instant::now();
app.ingest_snapshot(&Snapshot {
metrics: fixture_metrics_at(0),
received_at: t0,
});
app.ingest_snapshot(&Snapshot {
metrics: fixture_metrics_at(1),
received_at: t0 + Duration::from_secs(1),
});
app.ingest_listeners(ListenersSnapshot {
list: fixture_listeners(),
});
app
}
#[test]
fn snapshot_overview_80x24() {
let app = fixture_app();
let out = render_to_string(80, 24, &app, |f, area, app, skin| {
panes::overview::render(f, area, app, skin)
});
insta::assert_snapshot!(out);
}
#[test]
fn snapshot_overview_120x40() {
let app = fixture_app();
let out = render_to_string(120, 40, &app, |f, area, app, skin| {
panes::overview::render(f, area, app, skin)
});
insta::assert_snapshot!(out);
}
#[test]
fn snapshot_clusters_80x24() {
let app = fixture_app();
let out = render_to_string(80, 24, &app, |f, area, app, skin| {
panes::clusters::render(f, area, app, skin)
});
insta::assert_snapshot!(out);
}
#[test]
fn snapshot_clusters_empty_120x40() {
let app = App::new();
let out = render_to_string(120, 40, &app, |f, area, app, skin| {
panes::clusters::render(f, area, app, skin)
});
insta::assert_snapshot!(out);
}
#[test]
fn snapshot_backends_120x40() {
let app = fixture_app();
let out = render_to_string(120, 40, &app, |f, area, app, skin| {
panes::backends::render(f, area, app, skin)
});
insta::assert_snapshot!(out);
}
#[test]
fn snapshot_listeners_empty_80x24() {
let app = fixture_app(); let out = render_to_string(80, 24, &app, |f, area, app, skin| {
panes::listeners::render(f, area, app, skin)
});
insta::assert_snapshot!(out);
}
#[test]
fn snapshot_h2_120x40() {
let app = fixture_app();
let out = render_to_string(120, 40, &app, |f, area, app, skin| {
panes::h2::render(f, area, app, skin)
});
insta::assert_snapshot!(out);
}
#[test]
fn snapshot_events_empty_80x24() {
let app = App::new();
let out = render_to_string(80, 24, &app, |f, area, app, skin| {
panes::events::render(f, area, app, skin)
});
insta::assert_snapshot!(out);
}