feat(client): configurable metrics/process intervals with profile persistence; docs updated
This commit is contained in:
+10
-1
@@ -63,6 +63,7 @@ pub struct App {
|
||||
last_disks_poll: Instant,
|
||||
procs_interval: Duration,
|
||||
disks_interval: Duration,
|
||||
metrics_interval: Duration,
|
||||
|
||||
// For reconnects
|
||||
ws_url: String,
|
||||
@@ -94,10 +95,17 @@ impl App {
|
||||
.unwrap_or_else(Instant::now),
|
||||
procs_interval: Duration::from_secs(2),
|
||||
disks_interval: Duration::from_secs(5),
|
||||
metrics_interval: Duration::from_millis(500),
|
||||
ws_url: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_intervals(mut self, metrics_ms: Option<u64>, procs_ms: Option<u64>) -> Self {
|
||||
if let Some(m) = metrics_ms { self.metrics_interval = Duration::from_millis(m.max(100)); }
|
||||
if let Some(p) = procs_ms { self.procs_interval = Duration::from_millis(p.max(200)); }
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn run(
|
||||
&mut self,
|
||||
url: &str,
|
||||
@@ -284,7 +292,7 @@ impl App {
|
||||
terminal.draw(|f| self.draw(f))?;
|
||||
|
||||
// Tick rate
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
sleep(self.metrics_interval).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -471,6 +479,7 @@ impl Default for App {
|
||||
.unwrap_or_else(Instant::now),
|
||||
procs_interval: Duration::from_secs(2),
|
||||
disks_interval: Duration::from_secs(5),
|
||||
metrics_interval: Duration::from_millis(500),
|
||||
ws_url: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
+57
-9
@@ -19,6 +19,8 @@ pub(crate) struct ParsedArgs {
|
||||
save: bool,
|
||||
demo: bool,
|
||||
dry_run: bool, // hidden test helper: skip connecting
|
||||
metrics_interval_ms: Option<u64>,
|
||||
processes_interval_ms: Option<u64>,
|
||||
}
|
||||
|
||||
pub(crate) fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Result<ParsedArgs, String> {
|
||||
@@ -30,10 +32,12 @@ pub(crate) fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Result<Pars
|
||||
let mut save = false;
|
||||
let mut demo = false;
|
||||
let mut dry_run = false;
|
||||
let mut metrics_interval_ms: Option<u64> = None;
|
||||
let mut processes_interval_ms: Option<u64> = None;
|
||||
while let Some(arg) = it.next() {
|
||||
match arg.as_str() {
|
||||
"-h" | "--help" => {
|
||||
return Err(format!("Usage: {prog} [--tls-ca CERT_PEM|-t CERT_PEM] [--profile NAME|-P NAME] [--save] [--demo] [ws://HOST:PORT/ws]\n"));
|
||||
return Err(format!("Usage: {prog} [--tls-ca CERT_PEM|-t CERT_PEM] [--profile NAME|-P NAME] [--save] [--demo] [--metrics-interval-ms N] [--processes-interval-ms N] [ws://HOST:PORT/ws]\n"));
|
||||
}
|
||||
"--tls-ca" | "-t" => {
|
||||
tls_ca = it.next();
|
||||
@@ -51,6 +55,8 @@ pub(crate) fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Result<Pars
|
||||
// intentionally undocumented
|
||||
dry_run = true;
|
||||
}
|
||||
"--metrics-interval-ms" => { metrics_interval_ms = it.next().and_then(|v| v.parse().ok()); }
|
||||
"--processes-interval-ms" => { processes_interval_ms = it.next().and_then(|v| v.parse().ok()); }
|
||||
_ if arg.starts_with("--tls-ca=") => {
|
||||
if let Some((_, v)) = arg.split_once('=') {
|
||||
if !v.is_empty() {
|
||||
@@ -65,6 +71,8 @@ pub(crate) fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Result<Pars
|
||||
}
|
||||
}
|
||||
}
|
||||
_ if arg.starts_with("--metrics-interval-ms=") => { if let Some((_,v))=arg.split_once('='){ metrics_interval_ms = v.parse().ok(); } }
|
||||
_ if arg.starts_with("--processes-interval-ms=") => { if let Some((_,v))=arg.split_once('='){ processes_interval_ms = v.parse().ok(); } }
|
||||
_ => {
|
||||
if url.is_none() {
|
||||
url = Some(arg);
|
||||
@@ -81,6 +89,8 @@ pub(crate) fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Result<Pars
|
||||
save,
|
||||
demo,
|
||||
dry_run,
|
||||
metrics_interval_ms,
|
||||
processes_interval_ms,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -104,20 +114,24 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
};
|
||||
let resolved = req.resolve(&profiles_file);
|
||||
let mut profiles_mut = profiles_file.clone();
|
||||
let (url, tls_ca): (String, Option<String>) = match resolved {
|
||||
let (url, tls_ca, metrics_interval_ms, processes_interval_ms): (String, Option<String>, Option<u64>, Option<u64>) = match resolved {
|
||||
ResolveProfile::Direct(u, t) => {
|
||||
if let Some(name) = parsed.profile.as_ref() {
|
||||
let existing = profiles_mut.profiles.get(name);
|
||||
match existing {
|
||||
None => {
|
||||
let (mi, pi) = gather_intervals(parsed.metrics_interval_ms, parsed.processes_interval_ms)?;
|
||||
profiles_mut.profiles.insert(
|
||||
name.clone(),
|
||||
ProfileEntry {
|
||||
url: u.clone(),
|
||||
tls_ca: t.clone(),
|
||||
metrics_interval_ms: mi,
|
||||
processes_interval_ms: pi,
|
||||
},
|
||||
);
|
||||
let _ = save_profiles(&profiles_mut);
|
||||
(u, t, mi, pi)
|
||||
}
|
||||
Some(entry) => {
|
||||
let changed = entry.url != u || entry.tls_ca != t;
|
||||
@@ -130,22 +144,31 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
))
|
||||
};
|
||||
if overwrite {
|
||||
let (mi, pi) = gather_intervals(parsed.metrics_interval_ms, parsed.processes_interval_ms)?;
|
||||
profiles_mut.profiles.insert(
|
||||
name.clone(),
|
||||
ProfileEntry {
|
||||
url: u.clone(),
|
||||
tls_ca: t.clone(),
|
||||
metrics_interval_ms: mi,
|
||||
processes_interval_ms: pi,
|
||||
},
|
||||
);
|
||||
let _ = save_profiles(&profiles_mut);
|
||||
(u, t, mi, pi)
|
||||
} else {
|
||||
(u, t, entry.metrics_interval_ms, entry.processes_interval_ms)
|
||||
}
|
||||
}
|
||||
} else { (u, t, entry.metrics_interval_ms, entry.processes_interval_ms) }
|
||||
}
|
||||
}
|
||||
}
|
||||
(u, t)
|
||||
else { (u, t, parsed.metrics_interval_ms, parsed.processes_interval_ms) }
|
||||
}
|
||||
ResolveProfile::Loaded(u, t) => {
|
||||
let entry = profiles_mut.profiles.get(parsed.profile.as_ref().unwrap()).unwrap();
|
||||
(u, t, entry.metrics_interval_ms, entry.processes_interval_ms)
|
||||
}
|
||||
ResolveProfile::Loaded(u, t) => (u, t),
|
||||
ResolveProfile::PromptSelect(mut names) => {
|
||||
if !names.iter().any(|n| n == "demo") {
|
||||
names.push("demo".into());
|
||||
@@ -165,7 +188,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
return run_demo_mode(parsed.tls_ca.as_deref()).await;
|
||||
}
|
||||
if let Some(entry) = profiles_mut.profiles.get(name) {
|
||||
(entry.url.clone(), entry.tls_ca.clone())
|
||||
(entry.url.clone(), entry.tls_ca.clone(), entry.metrics_interval_ms, entry.processes_interval_ms)
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -191,22 +214,25 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
} else {
|
||||
Some(ca.trim().to_string())
|
||||
};
|
||||
profiles_mut.profiles.insert(
|
||||
let (mi, pi) = gather_intervals(parsed.metrics_interval_ms, parsed.processes_interval_ms)?;
|
||||
profiles_mut.profiles.insert(
|
||||
name.clone(),
|
||||
ProfileEntry {
|
||||
url: url.trim().to_string(),
|
||||
tls_ca: ca_opt.clone(),
|
||||
metrics_interval_ms: mi,
|
||||
processes_interval_ms: pi,
|
||||
},
|
||||
);
|
||||
let _ = save_profiles(&profiles_mut);
|
||||
(url.trim().to_string(), ca_opt)
|
||||
(url.trim().to_string(), ca_opt, mi, pi)
|
||||
}
|
||||
ResolveProfile::None => {
|
||||
eprintln!("No URL provided and no profiles to select.");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let mut app = App::new();
|
||||
let mut app = App::new().with_intervals(metrics_interval_ms, processes_interval_ms);
|
||||
if parsed.dry_run {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -231,6 +257,28 @@ fn prompt_string(prompt: &str) -> io::Result<String> {
|
||||
Ok(line)
|
||||
}
|
||||
|
||||
fn gather_intervals(arg_metrics: Option<u64>, arg_procs: Option<u64>) -> Result<(Option<u64>, Option<u64>), Box<dyn std::error::Error>> {
|
||||
let default_metrics = 500u64;
|
||||
let default_procs = 2000u64;
|
||||
let metrics = match arg_metrics {
|
||||
Some(v) => Some(v),
|
||||
None => {
|
||||
let inp = prompt_string(&format!("Metrics interval ms (default {default_metrics}, Enter for default): "))?;
|
||||
let t = inp.trim();
|
||||
if t.is_empty() { Some(default_metrics) } else { Some(t.parse()?) }
|
||||
}
|
||||
};
|
||||
let procs = match arg_procs {
|
||||
Some(v) => Some(v),
|
||||
None => {
|
||||
let inp = prompt_string(&format!("Processes interval ms (default {default_procs}, Enter for default): "))?;
|
||||
let t = inp.trim();
|
||||
if t.is_empty() { Some(default_procs) } else { Some(t.parse()?) }
|
||||
}
|
||||
};
|
||||
Ok((metrics, procs))
|
||||
}
|
||||
|
||||
// Demo mode implementation
|
||||
async fn run_demo_mode(_tls_ca: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let port = 3231;
|
||||
|
||||
@@ -9,6 +9,10 @@ pub struct ProfileEntry {
|
||||
pub url: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tls_ca: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub metrics_interval_ms: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub processes_interval_ms: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
|
||||
Reference in New Issue
Block a user