feat: metrics tree with process information

This commit is contained in:
Alex Zenla
2024-04-12 07:21:21 +00:00
parent 8bebda62ad
commit 6fa61a72be
20 changed files with 479 additions and 121 deletions

View File

@ -16,11 +16,15 @@ comfy-table = { workspace = true }
crossterm = { workspace = true }
ctrlc = { workspace = true, features = ["termination"] }
env_logger = { workspace = true }
fancy-duration = { workspace = true }
human_bytes = { workspace = true }
krata = { path = "../krata", version = "^0.0.8" }
log = { workspace = true }
prost-reflect = { workspace = true, features = ["serde"] }
prost-types = { workspace = true }
serde_json = { workspace = true }
serde_yaml = { workspace = true }
termtree = { workspace = true }
tokio = { workspace = true }
tokio-stream = { workspace = true }
tonic = { workspace = true }

View File

@ -1,22 +1,22 @@
use anyhow::Result;
use clap::{Parser, ValueEnum};
use comfy_table::{presets::UTF8_FULL_CONDENSED, Table};
use krata::{
events::EventStream,
v1::control::{
control_service_client::ControlServiceClient, GuestMetrics, ReadGuestMetricsRequest,
v1::{
common::GuestMetricNode,
control::{control_service_client::ControlServiceClient, ReadGuestMetricsRequest},
},
};
use tonic::transport::Channel;
use crate::format::{kv2line, proto2dynamic, proto2kv};
use crate::format::{kv2line, metrics_flat, metrics_tree, proto2dynamic};
use super::resolve_guest;
#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
enum MetricsFormat {
Table,
Tree,
Json,
JsonPretty,
Yaml,
@ -26,7 +26,7 @@ enum MetricsFormat {
#[derive(Parser)]
#[command(about = "Read metrics from the guest")]
pub struct MetricsCommand {
#[arg(short, long, default_value = "table", help = "Output format")]
#[arg(short, long, default_value = "tree", help = "Output format")]
format: MetricsFormat,
#[arg(help = "Guest to read metrics for, either the name or the uuid")]
guest: String,
@ -39,19 +39,19 @@ impl MetricsCommand {
_events: EventStream,
) -> Result<()> {
let guest_id: String = resolve_guest(&mut client, &self.guest).await?;
let metrics = client
let root = client
.read_guest_metrics(ReadGuestMetricsRequest { guest_id })
.await?
.into_inner()
.metrics
.root
.unwrap_or_default();
match self.format {
MetricsFormat::Table => {
self.print_metrics_table(metrics)?;
MetricsFormat::Tree => {
self.print_metrics_tree(root)?;
}
MetricsFormat::Json | MetricsFormat::JsonPretty | MetricsFormat::Yaml => {
let value = serde_json::to_value(proto2dynamic(metrics)?)?;
let value = serde_json::to_value(proto2dynamic(root)?)?;
let encoded = if self.format == MetricsFormat::JsonPretty {
serde_json::to_string_pretty(&value)?
} else if self.format == MetricsFormat::Yaml {
@ -63,29 +63,21 @@ impl MetricsCommand {
}
MetricsFormat::KeyValue => {
self.print_key_value(metrics)?;
self.print_key_value(root)?;
}
}
Ok(())
}
fn print_metrics_table(&self, metrics: GuestMetrics) -> Result<()> {
let mut table = Table::new();
table.load_preset(UTF8_FULL_CONDENSED);
table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic);
table.set_header(vec!["metric", "value"]);
let kvs = proto2kv(metrics)?;
for (key, value) in kvs {
table.add_row(vec![key, value]);
}
println!("{}", table);
fn print_metrics_tree(&self, root: GuestMetricNode) -> Result<()> {
print!("{}", metrics_tree(root));
Ok(())
}
fn print_key_value(&self, metrics: GuestMetrics) -> Result<()> {
let kvs = proto2kv(metrics)?;
println!("{}", kv2line(kvs),);
fn print_key_value(&self, metrics: GuestMetricNode) -> Result<()> {
let kvs = metrics_flat(metrics);
println!("{}", kv2line(kvs));
Ok(())
}
}

View File

@ -1,8 +1,12 @@
use std::collections::HashMap;
use std::{collections::HashMap, time::Duration};
use anyhow::Result;
use krata::v1::common::{Guest, GuestStatus};
use prost_reflect::{DynamicMessage, ReflectMessage, Value};
use fancy_duration::FancyDuration;
use human_bytes::human_bytes;
use krata::v1::common::{Guest, GuestMetricFormat, GuestMetricNode, GuestStatus};
use prost_reflect::{DynamicMessage, FieldDescriptor, ReflectMessage, Value as ReflectValue};
use prost_types::Value;
use termtree::Tree;
pub fn proto2dynamic(proto: impl ReflectMessage) -> Result<DynamicMessage> {
Ok(DynamicMessage::decode(
@ -15,38 +19,56 @@ pub fn proto2kv(proto: impl ReflectMessage) -> Result<HashMap<String, String>> {
let message = proto2dynamic(proto)?;
let mut map = HashMap::new();
fn crawl(prefix: &str, map: &mut HashMap<String, String>, message: &DynamicMessage) {
for (field, value) in message.fields() {
let path = if prefix.is_empty() {
field.name().to_string()
} else {
format!("{}.{}", prefix, field.name())
};
match value {
Value::Message(child) => {
crawl(&path, map, child);
fn crawl(
prefix: String,
field: Option<&FieldDescriptor>,
map: &mut HashMap<String, String>,
value: &ReflectValue,
) {
match value {
ReflectValue::Message(child) => {
for (field, field_value) in child.fields() {
let path = if prefix.is_empty() {
field.json_name().to_string()
} else {
format!("{}.{}", prefix, field.json_name())
};
crawl(path, Some(&field), map, field_value);
}
}
Value::EnumNumber(number) => {
if let Some(e) = field.kind().as_enum() {
ReflectValue::EnumNumber(number) => {
if let Some(kind) = field.map(|x| x.kind()) {
if let Some(e) = kind.as_enum() {
if let Some(value) = e.get_value(*number) {
map.insert(path, value.name().to_string());
map.insert(prefix, value.name().to_string());
}
}
}
}
Value::String(value) => {
map.insert(path, value.clone());
}
ReflectValue::String(value) => {
map.insert(prefix.to_string(), value.clone());
}
_ => {
map.insert(path, value.to_string());
ReflectValue::List(value) => {
for (x, value) in value.iter().enumerate() {
crawl(format!("{}.{}", prefix, x), field, map, value);
}
}
_ => {
map.insert(prefix.to_string(), value.to_string());
}
}
}
crawl("", &mut map, &message);
crawl(
"".to_string(),
None,
&mut map,
&ReflectValue::Message(message),
);
Ok(map)
}
@ -85,3 +107,63 @@ pub fn guest_simple_line(guest: &Guest) -> String {
let ipv6 = network.map(|x| x.guest_ipv6.as_str()).unwrap_or("");
format!("{}\t{}\t{}\t{}\t{}", guest.id, state, name, ipv4, ipv6)
}
fn metrics_value_string(value: Value) -> String {
proto2dynamic(value)
.map(|x| serde_json::to_string(&x).ok())
.ok()
.flatten()
.unwrap_or_default()
}
fn metrics_value_numeric(value: Value) -> f64 {
let string = metrics_value_string(value);
string.parse::<f64>().ok().unwrap_or(f64::NAN)
}
fn metrics_value_pretty(value: Value, format: GuestMetricFormat) -> String {
match format {
GuestMetricFormat::Bytes => human_bytes(metrics_value_numeric(value)),
GuestMetricFormat::Integer => (metrics_value_numeric(value) as u64).to_string(),
GuestMetricFormat::DurationSeconds => {
FancyDuration(Duration::from_secs_f64(metrics_value_numeric(value))).to_string()
}
_ => metrics_value_string(value),
}
}
fn metrics_flat_internal(prefix: &str, node: GuestMetricNode, map: &mut HashMap<String, String>) {
if let Some(value) = node.value {
map.insert(prefix.to_string(), metrics_value_string(value));
}
for child in node.children {
let path = if prefix.is_empty() {
child.name.to_string()
} else {
format!("{}.{}", prefix, child.name)
};
metrics_flat_internal(&path, child, map);
}
}
pub fn metrics_flat(root: GuestMetricNode) -> HashMap<String, String> {
let mut map = HashMap::new();
metrics_flat_internal("", root, &mut map);
map
}
pub fn metrics_tree(node: GuestMetricNode) -> Tree<String> {
let mut name = node.name.to_string();
let format = node.format();
if let Some(value) = node.value {
let value_string = metrics_value_pretty(value, format);
name.push_str(&format!(": {}", value_string));
}
let mut tree = Tree::new(name);
for child in node.children {
tree.push(metrics_tree(child));
}
tree
}