mirror of
https://github.com/edera-dev/krata.git
synced 2025-08-02 12:50:54 +00:00
feat: basic kratactl top command (#72)
* feat: basic kratactl top command * fix: use magic bytes 0xff 0xff in idm to improve reliability
This commit is contained in:
parent
1627cbcdd7
commit
0a6a112133
132
Cargo.lock
generated
132
Cargo.lock
generated
@ -17,6 +17,18 @@ version = "1.0.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ahash"
|
||||||
|
version = "0.8.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
|
"version_check",
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.1.3"
|
version = "1.1.3"
|
||||||
@ -26,6 +38,12 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "allocator-api2"
|
||||||
|
version = "0.2.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstream"
|
name = "anstream"
|
||||||
version = "0.6.13"
|
version = "0.6.13"
|
||||||
@ -302,6 +320,21 @@ version = "1.6.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
|
checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cassowary"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "castaway"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc"
|
||||||
|
dependencies = [
|
||||||
|
"rustversion",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.0.90"
|
version = "1.0.90"
|
||||||
@ -421,6 +454,19 @@ dependencies = [
|
|||||||
"unicode-width",
|
"unicode-width",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "compact_str"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f"
|
||||||
|
dependencies = [
|
||||||
|
"castaway",
|
||||||
|
"cfg-if",
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"static_assertions",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "console"
|
name = "console"
|
||||||
version = "0.15.8"
|
version = "0.15.8"
|
||||||
@ -491,6 +537,7 @@ checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.5.0",
|
"bitflags 2.5.0",
|
||||||
"crossterm_winapi",
|
"crossterm_winapi",
|
||||||
|
"futures-core",
|
||||||
"libc",
|
"libc",
|
||||||
"mio",
|
"mio",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
@ -999,6 +1046,10 @@ name = "hashbrown"
|
|||||||
version = "0.14.3"
|
version = "0.14.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
|
checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
|
||||||
|
dependencies = [
|
||||||
|
"ahash",
|
||||||
|
"allocator-api2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heapless"
|
name = "heapless"
|
||||||
@ -1255,6 +1306,12 @@ dependencies = [
|
|||||||
"unicode-width",
|
"unicode-width",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indoc"
|
||||||
|
version = "2.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "instant"
|
name = "instant"
|
||||||
version = "0.1.12"
|
version = "0.1.12"
|
||||||
@ -1367,6 +1424,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"prost-reflect",
|
"prost-reflect",
|
||||||
"prost-types",
|
"prost-types",
|
||||||
|
"ratatui",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
"termtree",
|
"termtree",
|
||||||
@ -1638,6 +1696,15 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lru"
|
||||||
|
version = "0.12.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc"
|
||||||
|
dependencies = [
|
||||||
|
"hashbrown 0.14.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lzma-sys"
|
name = "lzma-sys"
|
||||||
version = "0.1.20"
|
version = "0.1.20"
|
||||||
@ -2193,6 +2260,26 @@ dependencies = [
|
|||||||
"getrandom",
|
"getrandom",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ratatui"
|
||||||
|
version = "0.26.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bcb12f8fbf6c62614b0d56eb352af54f6a22410c3b079eb53ee93c7b97dd31d8"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.5.0",
|
||||||
|
"cassowary",
|
||||||
|
"compact_str",
|
||||||
|
"crossterm",
|
||||||
|
"indoc",
|
||||||
|
"itertools",
|
||||||
|
"lru",
|
||||||
|
"paste",
|
||||||
|
"stability",
|
||||||
|
"strum",
|
||||||
|
"unicode-segmentation",
|
||||||
|
"unicode-width",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rayon"
|
name = "rayon"
|
||||||
version = "1.10.0"
|
version = "1.10.0"
|
||||||
@ -2626,12 +2713,28 @@ version = "0.9.8"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "stability"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ebd1b177894da2a2d9120208c3386066af06a488255caabc5de8ddca22dbc3ce"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"syn 1.0.109",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stable_deref_trait"
|
name = "stable_deref_trait"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "static_assertions"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.10.0"
|
version = "0.10.0"
|
||||||
@ -2649,6 +2752,9 @@ name = "strum"
|
|||||||
version = "0.26.2"
|
version = "0.26.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29"
|
checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29"
|
||||||
|
dependencies = [
|
||||||
|
"strum_macros",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strum_macros"
|
name = "strum_macros"
|
||||||
@ -3040,6 +3146,12 @@ dependencies = [
|
|||||||
"tinyvec",
|
"tinyvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-segmentation"
|
||||||
|
version = "1.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-width"
|
name = "unicode-width"
|
||||||
version = "0.1.11"
|
version = "0.1.11"
|
||||||
@ -3430,6 +3542,26 @@ dependencies = [
|
|||||||
"lzma-sys",
|
"lzma-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy"
|
||||||
|
version = "0.7.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy-derive"
|
||||||
|
version = "0.7.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.57",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zeroize"
|
name = "zeroize"
|
||||||
version = "1.7.0"
|
version = "1.7.0"
|
||||||
|
@ -60,6 +60,7 @@ prost-build = "0.12.4"
|
|||||||
prost-reflect-build = "0.13.0"
|
prost-reflect-build = "0.13.0"
|
||||||
prost-types = "0.12.4"
|
prost-types = "0.12.4"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
|
ratatui = "0.26.1"
|
||||||
redb = "2.0.0"
|
redb = "2.0.0"
|
||||||
rtnetlink = "0.14.1"
|
rtnetlink = "0.14.1"
|
||||||
scopeguard = "1.2.0"
|
scopeguard = "1.2.0"
|
||||||
|
@ -13,7 +13,7 @@ anyhow = { workspace = true }
|
|||||||
async-stream = { workspace = true }
|
async-stream = { workspace = true }
|
||||||
clap = { workspace = true }
|
clap = { workspace = true }
|
||||||
comfy-table = { workspace = true }
|
comfy-table = { workspace = true }
|
||||||
crossterm = { workspace = true }
|
crossterm = { workspace = true, features = ["event-stream"] }
|
||||||
ctrlc = { workspace = true, features = ["termination"] }
|
ctrlc = { workspace = true, features = ["termination"] }
|
||||||
env_logger = { workspace = true }
|
env_logger = { workspace = true }
|
||||||
fancy-duration = { workspace = true }
|
fancy-duration = { workspace = true }
|
||||||
@ -23,6 +23,7 @@ krata = { path = "../krata", version = "^0.0.8" }
|
|||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
prost-reflect = { workspace = true, features = ["serde"] }
|
prost-reflect = { workspace = true, features = ["serde"] }
|
||||||
prost-types = { workspace = true }
|
prost-types = { workspace = true }
|
||||||
|
ratatui = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
serde_yaml = { workspace = true }
|
serde_yaml = { workspace = true }
|
||||||
termtree = { workspace = true }
|
termtree = { workspace = true }
|
||||||
|
@ -6,6 +6,7 @@ pub mod list;
|
|||||||
pub mod logs;
|
pub mod logs;
|
||||||
pub mod metrics;
|
pub mod metrics;
|
||||||
pub mod resolve;
|
pub mod resolve;
|
||||||
|
pub mod top;
|
||||||
pub mod watch;
|
pub mod watch;
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
@ -20,7 +21,7 @@ use tonic::{transport::Channel, Request};
|
|||||||
use self::{
|
use self::{
|
||||||
attach::AttachCommand, destroy::DestroyCommand, idm_snoop::IdmSnoopCommand,
|
attach::AttachCommand, destroy::DestroyCommand, idm_snoop::IdmSnoopCommand,
|
||||||
launch::LauchCommand, list::ListCommand, logs::LogsCommand, metrics::MetricsCommand,
|
launch::LauchCommand, list::ListCommand, logs::LogsCommand, metrics::MetricsCommand,
|
||||||
resolve::ResolveCommand, watch::WatchCommand,
|
resolve::ResolveCommand, top::TopCommand, watch::WatchCommand,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
@ -52,6 +53,7 @@ pub enum Commands {
|
|||||||
Resolve(ResolveCommand),
|
Resolve(ResolveCommand),
|
||||||
Metrics(MetricsCommand),
|
Metrics(MetricsCommand),
|
||||||
IdmSnoop(IdmSnoopCommand),
|
IdmSnoop(IdmSnoopCommand),
|
||||||
|
Top(TopCommand),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ControlCommand {
|
impl ControlCommand {
|
||||||
@ -95,6 +97,10 @@ impl ControlCommand {
|
|||||||
Commands::IdmSnoop(snoop) => {
|
Commands::IdmSnoop(snoop) => {
|
||||||
snoop.run(client, events).await?;
|
snoop.run(client, events).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Commands::Top(top) => {
|
||||||
|
top.run(client, events).await?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
215
crates/ctl/src/cli/top.rs
Normal file
215
crates/ctl/src/cli/top.rs
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use clap::Parser;
|
||||||
|
use krata::{events::EventStream, v1::control::control_service_client::ControlServiceClient};
|
||||||
|
use std::{
|
||||||
|
io::{self, stdout, Stdout},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
use tokio::select;
|
||||||
|
use tokio_stream::StreamExt;
|
||||||
|
use tonic::transport::Channel;
|
||||||
|
|
||||||
|
use crossterm::{
|
||||||
|
event::{Event, KeyCode, KeyEvent, KeyEventKind},
|
||||||
|
execute,
|
||||||
|
terminal::*,
|
||||||
|
};
|
||||||
|
use ratatui::{
|
||||||
|
prelude::*,
|
||||||
|
symbols::border,
|
||||||
|
widgets::{
|
||||||
|
block::{Position, Title},
|
||||||
|
Block, Borders, Row, Table, TableState,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
format::guest_status_text,
|
||||||
|
metrics::{
|
||||||
|
lookup_metric_value, MultiMetricCollector, MultiMetricCollectorHandle, MultiMetricState,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(about = "Dashboard for running guests")]
|
||||||
|
pub struct TopCommand {}
|
||||||
|
|
||||||
|
pub type Tui = Terminal<CrosstermBackend<Stdout>>;
|
||||||
|
|
||||||
|
impl TopCommand {
|
||||||
|
pub async fn run(
|
||||||
|
self,
|
||||||
|
client: ControlServiceClient<Channel>,
|
||||||
|
events: EventStream,
|
||||||
|
) -> Result<()> {
|
||||||
|
let collector = MultiMetricCollector::new(client, events, Duration::from_millis(200))?;
|
||||||
|
let collector = collector.launch().await?;
|
||||||
|
let mut tui = TopCommand::init()?;
|
||||||
|
let mut app = TopApp {
|
||||||
|
metrics: MultiMetricState { guests: vec![] },
|
||||||
|
exit: false,
|
||||||
|
table: TableState::new(),
|
||||||
|
};
|
||||||
|
app.run(collector, &mut tui).await?;
|
||||||
|
TopCommand::restore()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init() -> io::Result<Tui> {
|
||||||
|
execute!(stdout(), EnterAlternateScreen)?;
|
||||||
|
enable_raw_mode()?;
|
||||||
|
Terminal::new(CrosstermBackend::new(stdout()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn restore() -> io::Result<()> {
|
||||||
|
execute!(stdout(), LeaveAlternateScreen)?;
|
||||||
|
disable_raw_mode()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TopApp {
|
||||||
|
table: TableState,
|
||||||
|
metrics: MultiMetricState,
|
||||||
|
exit: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TopApp {
|
||||||
|
pub async fn run(
|
||||||
|
&mut self,
|
||||||
|
mut collector: MultiMetricCollectorHandle,
|
||||||
|
terminal: &mut Tui,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut events = crossterm::event::EventStream::new();
|
||||||
|
|
||||||
|
while !self.exit {
|
||||||
|
terminal.draw(|frame| self.render_frame(frame))?;
|
||||||
|
|
||||||
|
select! {
|
||||||
|
x = collector.receiver.recv() => match x {
|
||||||
|
Some(state) => {
|
||||||
|
self.metrics = state;
|
||||||
|
},
|
||||||
|
|
||||||
|
None => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
x = events.next() => match x {
|
||||||
|
Some(event) => {
|
||||||
|
let event = event?;
|
||||||
|
self.handle_event(event)?;
|
||||||
|
},
|
||||||
|
|
||||||
|
None => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_frame(&mut self, frame: &mut Frame) {
|
||||||
|
frame.render_widget(self, frame.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_event(&mut self, event: Event) -> io::Result<()> {
|
||||||
|
match event {
|
||||||
|
Event::Key(key_event) if key_event.kind == KeyEventKind::Press => {
|
||||||
|
self.handle_key_event(key_event)
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exit(&mut self) {
|
||||||
|
self.exit = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_key_event(&mut self, key_event: KeyEvent) {
|
||||||
|
if let KeyCode::Char('q') = key_event.code {
|
||||||
|
self.exit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for &mut TopApp {
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
let title = Title::from(" krata hypervisor ".bold());
|
||||||
|
let instructions = Title::from(vec![" Quit ".into(), "<Q> ".blue().bold()]);
|
||||||
|
let block = Block::default()
|
||||||
|
.title(title.alignment(Alignment::Center))
|
||||||
|
.title(
|
||||||
|
instructions
|
||||||
|
.alignment(Alignment::Center)
|
||||||
|
.position(Position::Bottom),
|
||||||
|
)
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_set(border::THICK);
|
||||||
|
|
||||||
|
let mut rows = vec![];
|
||||||
|
|
||||||
|
for ms in &self.metrics.guests {
|
||||||
|
let Some(ref spec) = ms.guest.spec else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(ref state) = ms.guest.state else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let memory_total = ms
|
||||||
|
.root
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|root| lookup_metric_value(root, "system/memory/total"));
|
||||||
|
let memory_used = ms
|
||||||
|
.root
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|root| lookup_metric_value(root, "system/memory/used"));
|
||||||
|
let memory_free = ms
|
||||||
|
.root
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|root| lookup_metric_value(root, "system/memory/free"));
|
||||||
|
|
||||||
|
let row = Row::new(vec![
|
||||||
|
spec.name.clone(),
|
||||||
|
ms.guest.id.clone(),
|
||||||
|
guest_status_text(state.status()),
|
||||||
|
memory_total.unwrap_or_default(),
|
||||||
|
memory_used.unwrap_or_default(),
|
||||||
|
memory_free.unwrap_or_default(),
|
||||||
|
]);
|
||||||
|
rows.push(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
let widths = [
|
||||||
|
Constraint::Min(8),
|
||||||
|
Constraint::Min(8),
|
||||||
|
Constraint::Min(8),
|
||||||
|
Constraint::Min(8),
|
||||||
|
Constraint::Min(8),
|
||||||
|
Constraint::Min(8),
|
||||||
|
];
|
||||||
|
|
||||||
|
let table = Table::new(rows, widths)
|
||||||
|
.header(
|
||||||
|
Row::new(vec![
|
||||||
|
"name",
|
||||||
|
"id",
|
||||||
|
"status",
|
||||||
|
"total memory",
|
||||||
|
"used memory",
|
||||||
|
"free memory",
|
||||||
|
])
|
||||||
|
.style(Style::new().bold())
|
||||||
|
.bottom_margin(1),
|
||||||
|
)
|
||||||
|
.column_spacing(1)
|
||||||
|
.block(block);
|
||||||
|
|
||||||
|
StatefulWidget::render(table, area, buf, &mut self.table);
|
||||||
|
}
|
||||||
|
}
|
@ -121,7 +121,7 @@ fn metrics_value_numeric(value: Value) -> f64 {
|
|||||||
string.parse::<f64>().ok().unwrap_or(f64::NAN)
|
string.parse::<f64>().ok().unwrap_or(f64::NAN)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn metrics_value_pretty(value: Value, format: GuestMetricFormat) -> String {
|
pub fn metrics_value_pretty(value: Value, format: GuestMetricFormat) -> String {
|
||||||
match format {
|
match format {
|
||||||
GuestMetricFormat::Bytes => human_bytes(metrics_value_numeric(value)),
|
GuestMetricFormat::Bytes => human_bytes(metrics_value_numeric(value)),
|
||||||
GuestMetricFormat::Integer => (metrics_value_numeric(value) as u64).to_string(),
|
GuestMetricFormat::Integer => (metrics_value_numeric(value) as u64).to_string(),
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
pub mod cli;
|
pub mod cli;
|
||||||
pub mod console;
|
pub mod console;
|
||||||
pub mod format;
|
pub mod format;
|
||||||
|
pub mod metrics;
|
||||||
|
159
crates/ctl/src/metrics.rs
Normal file
159
crates/ctl/src/metrics.rs
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use krata::{
|
||||||
|
events::EventStream,
|
||||||
|
v1::{
|
||||||
|
common::{Guest, GuestMetricNode, GuestStatus},
|
||||||
|
control::{
|
||||||
|
control_service_client::ControlServiceClient, watch_events_reply::Event,
|
||||||
|
ListGuestsRequest, ReadGuestMetricsRequest,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use log::error;
|
||||||
|
use std::time::Duration;
|
||||||
|
use tokio::{
|
||||||
|
select,
|
||||||
|
sync::mpsc::{channel, Receiver, Sender},
|
||||||
|
task::JoinHandle,
|
||||||
|
time::{sleep, timeout},
|
||||||
|
};
|
||||||
|
use tonic::transport::Channel;
|
||||||
|
|
||||||
|
use crate::format::metrics_value_pretty;
|
||||||
|
|
||||||
|
pub struct MetricState {
|
||||||
|
pub guest: Guest,
|
||||||
|
pub root: Option<GuestMetricNode>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MultiMetricState {
|
||||||
|
pub guests: Vec<MetricState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MultiMetricCollector {
|
||||||
|
client: ControlServiceClient<Channel>,
|
||||||
|
events: EventStream,
|
||||||
|
period: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MultiMetricCollectorHandle {
|
||||||
|
pub receiver: Receiver<MultiMetricState>,
|
||||||
|
task: JoinHandle<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for MultiMetricCollectorHandle {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.task.abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MultiMetricCollector {
|
||||||
|
pub fn new(
|
||||||
|
client: ControlServiceClient<Channel>,
|
||||||
|
events: EventStream,
|
||||||
|
period: Duration,
|
||||||
|
) -> Result<MultiMetricCollector> {
|
||||||
|
Ok(MultiMetricCollector {
|
||||||
|
client,
|
||||||
|
events,
|
||||||
|
period,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn launch(mut self) -> Result<MultiMetricCollectorHandle> {
|
||||||
|
let (sender, receiver) = channel::<MultiMetricState>(100);
|
||||||
|
let task = tokio::task::spawn(async move {
|
||||||
|
if let Err(error) = self.process(sender).await {
|
||||||
|
error!("failed to process multi metric collector: {}", error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Ok(MultiMetricCollectorHandle { receiver, task })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn process(&mut self, sender: Sender<MultiMetricState>) -> Result<()> {
|
||||||
|
let mut events = self.events.subscribe();
|
||||||
|
let mut guests: Vec<Guest> = self
|
||||||
|
.client
|
||||||
|
.list_guests(ListGuestsRequest {})
|
||||||
|
.await?
|
||||||
|
.into_inner()
|
||||||
|
.guests;
|
||||||
|
loop {
|
||||||
|
let collect = select! {
|
||||||
|
x = events.recv() => match x {
|
||||||
|
Ok(event) => {
|
||||||
|
if let Event::GuestChanged(changed) = event {
|
||||||
|
let Some(guest) = changed.guest else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(ref state) = guest.state else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
guests.retain(|x| x.id != guest.id);
|
||||||
|
if state.status() != GuestStatus::Destroying {
|
||||||
|
guests.push(guest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
},
|
||||||
|
|
||||||
|
Err(error) => {
|
||||||
|
return Err(error.into());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_ = sleep(self.period) => {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if !collect {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut metrics = Vec::new();
|
||||||
|
for guest in &guests {
|
||||||
|
let Some(ref state) = guest.state else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if state.status() != GuestStatus::Started {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let root = timeout(
|
||||||
|
Duration::from_secs(5),
|
||||||
|
self.client.read_guest_metrics(ReadGuestMetricsRequest {
|
||||||
|
guest_id: guest.id.clone(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.and_then(|x| x.ok())
|
||||||
|
.map(|x| x.into_inner())
|
||||||
|
.and_then(|x| x.root);
|
||||||
|
metrics.push(MetricState {
|
||||||
|
guest: guest.clone(),
|
||||||
|
root,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
sender.send(MultiMetricState { guests: metrics }).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn lookup<'a>(node: &'a GuestMetricNode, path: &str) -> Option<&'a GuestMetricNode> {
|
||||||
|
let Some((what, b)) = path.split_once('/') else {
|
||||||
|
return node.children.iter().find(|x| x.name == path);
|
||||||
|
};
|
||||||
|
let next = node.children.iter().find(|x| x.name == what)?;
|
||||||
|
return lookup(next, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn lookup_metric_value(node: &GuestMetricNode, path: &str) -> Option<String> {
|
||||||
|
lookup(node, path).and_then(|x| {
|
||||||
|
x.value
|
||||||
|
.as_ref()
|
||||||
|
.map(|v| metrics_value_pretty(v.clone(), x.format()))
|
||||||
|
})
|
||||||
|
}
|
@ -120,16 +120,22 @@ impl DaemonIdm {
|
|||||||
if let Some(data) = data {
|
if let Some(data) = data {
|
||||||
let buffer = buffers.entry(domid).or_insert_with_key(|_| BytesMut::new());
|
let buffer = buffers.entry(domid).or_insert_with_key(|_| BytesMut::new());
|
||||||
buffer.extend_from_slice(&data);
|
buffer.extend_from_slice(&data);
|
||||||
if buffer.len() < 4 {
|
if buffer.len() < 6 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let size = (buffer[0] as u32 | (buffer[1] as u32) << 8 | (buffer[2] as u32) << 16 | (buffer[3] as u32) << 24) as usize;
|
|
||||||
let needed = size + 4;
|
if buffer[0] != 0xff || buffer[1] != 0xff {
|
||||||
|
buffer.clear();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let size = (buffer[2] as u32 | (buffer[3] as u32) << 8 | (buffer[4] as u32) << 16 | (buffer[5] as u32) << 24) as usize;
|
||||||
|
let needed = size + 6;
|
||||||
if buffer.len() < needed {
|
if buffer.len() < needed {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let mut packet = buffer.split_to(needed);
|
let mut packet = buffer.split_to(needed);
|
||||||
packet.advance(4);
|
packet.advance(6);
|
||||||
match IdmPacket::decode(packet) {
|
match IdmPacket::decode(packet) {
|
||||||
Ok(packet) => {
|
Ok(packet) => {
|
||||||
let _ = client_or_create(domid, &self.tx_sender, &self.clients, &self.feeds).await?;
|
let _ = client_or_create(domid, &self.tx_sender, &self.clients, &self.feeds).await?;
|
||||||
@ -159,12 +165,14 @@ impl DaemonIdm {
|
|||||||
x = self.tx_receiver.recv() => match x {
|
x = self.tx_receiver.recv() => match x {
|
||||||
Some((domid, packet)) => {
|
Some((domid, packet)) => {
|
||||||
let data = packet.encode_to_vec();
|
let data = packet.encode_to_vec();
|
||||||
let mut buffer = vec![0u8; 4];
|
let mut buffer = vec![0u8; 6];
|
||||||
let length = data.len() as u32;
|
let length = data.len() as u32;
|
||||||
buffer[0] = length as u8;
|
buffer[0] = 0xff;
|
||||||
buffer[1] = (length << 8) as u8;
|
buffer[1] = 0xff;
|
||||||
buffer[2] = (length << 16) as u8;
|
buffer[2] = length as u8;
|
||||||
buffer[3] = (length << 24) as u8;
|
buffer[3] = (length << 8) as u8;
|
||||||
|
buffer[4] = (length << 16) as u8;
|
||||||
|
buffer[5] = (length << 24) as u8;
|
||||||
buffer.extend_from_slice(&data);
|
buffer.extend_from_slice(&data);
|
||||||
self.tx_raw_sender.send((domid, buffer)).await?;
|
self.tx_raw_sender.send((domid, buffer)).await?;
|
||||||
let _ = self.snoop_sender.send(DaemonIdmSnoopPacket { from: 0, to: domid, packet });
|
let _ = self.snoop_sender.send(DaemonIdmSnoopPacket { from: 0, to: domid, packet });
|
||||||
|
@ -69,6 +69,14 @@ impl IdmBackend for IdmFileBackend {
|
|||||||
async fn recv(&mut self) -> Result<IdmPacket> {
|
async fn recv(&mut self) -> Result<IdmPacket> {
|
||||||
let mut fd = self.read_fd.lock().await;
|
let mut fd = self.read_fd.lock().await;
|
||||||
let mut guard = fd.readable_mut().await?;
|
let mut guard = fd.readable_mut().await?;
|
||||||
|
let b1 = guard.get_inner_mut().read_u8().await?;
|
||||||
|
if b1 != 0xff {
|
||||||
|
return Ok(IdmPacket::default());
|
||||||
|
}
|
||||||
|
let b2 = guard.get_inner_mut().read_u8().await?;
|
||||||
|
if b2 != 0xff {
|
||||||
|
return Ok(IdmPacket::default());
|
||||||
|
}
|
||||||
let size = guard.get_inner_mut().read_u32_le().await?;
|
let size = guard.get_inner_mut().read_u32_le().await?;
|
||||||
if size == 0 {
|
if size == 0 {
|
||||||
return Ok(IdmPacket::default());
|
return Ok(IdmPacket::default());
|
||||||
@ -84,6 +92,7 @@ impl IdmBackend for IdmFileBackend {
|
|||||||
async fn send(&mut self, packet: IdmPacket) -> Result<()> {
|
async fn send(&mut self, packet: IdmPacket) -> Result<()> {
|
||||||
let mut file = self.write.lock().await;
|
let mut file = self.write.lock().await;
|
||||||
let data = packet.encode_to_vec();
|
let data = packet.encode_to_vec();
|
||||||
|
file.write_all(&[0xff, 0xff]).await?;
|
||||||
file.write_u32_le(data.len() as u32).await?;
|
file.write_u32_le(data.len() as u32).await?;
|
||||||
file.write_all(&data).await?;
|
file.write_all(&data).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -448,6 +448,10 @@ impl KrataChannelBackendProcessor {
|
|||||||
error!("channel for domid {} has an invalid input space of {}", self.domid, space);
|
error!("channel for domid {} has an invalid input space of {}", self.domid, space);
|
||||||
}
|
}
|
||||||
let free = XenConsoleInterface::INPUT_SIZE.wrapping_sub(space);
|
let free = XenConsoleInterface::INPUT_SIZE.wrapping_sub(space);
|
||||||
|
if free == 0 {
|
||||||
|
sleep(Duration::from_micros(100)).await;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let want = data.len().min(free);
|
let want = data.len().min(free);
|
||||||
let buffer = &data[index..want];
|
let buffer = &data[index..want];
|
||||||
for b in buffer {
|
for b in buffer {
|
||||||
|
Loading…
Reference in New Issue
Block a user