use crate::control::protocol::Response;
use crate::identity::{NodeAddr, PeerIdentity};
use crate::node::Node;
use crate::node::stats_history::{ALL_METRICS, ALL_PEER_METRICS, Granularity, Metric, PeerMetric};
use serde_json::{Value, json};
use std::str::FromStr;
use std::time::Duration;
fn parse_peer_npub(s: &str) -> Result<NodeAddr, String> {
PeerIdentity::from_npub(s)
.map(|p| *p.node_addr())
.map_err(|e| format!("invalid peer npub: {e}"))
}
pub fn show_stats_list() -> Value {
let metrics: Vec<Value> = ALL_METRICS
.iter()
.map(|m| {
json!({
"name": m.name(),
"unit": m.unit(),
"scope": "node",
})
})
.chain(ALL_PEER_METRICS.iter().map(|m| {
json!({
"name": m.name(),
"unit": m.unit(),
"scope": "peer",
})
}))
.collect();
json!({
"metrics": metrics,
"fast_ring_seconds": crate::node::stats_history::FAST_RING_CAPACITY,
"slow_ring_minutes": crate::node::stats_history::SLOW_RING_CAPACITY,
"peer_retention_seconds": crate::node::stats_history::PEER_EVICTION_SECS,
})
}
pub fn show_stats_history(node: &Node, params: Option<&Value>) -> Response {
use Response;
let Some(params) = params else {
return Response::error("missing params for show_stats_history");
};
let metric_name = match params.get("metric").and_then(|v| v.as_str()) {
Some(v) => v,
None => return Response::error("missing 'metric' parameter"),
};
let window_str = params
.get("window")
.and_then(|v| v.as_str())
.unwrap_or("10m");
let window = match parse_duration(window_str) {
Ok(d) => d,
Err(e) => return Response::error(e),
};
let granularity_str = params
.get("granularity")
.and_then(|v| v.as_str())
.unwrap_or("1s");
let granularity = match Granularity::from_str(granularity_str) {
Ok(g) => g,
Err(e) => return Response::error(e),
};
let peer_npub = params.get("peer").and_then(|v| v.as_str());
let hist = node.stats_history();
if let Some(npub) = peer_npub {
let addr = match parse_peer_npub(npub) {
Ok(a) => a,
Err(e) => return Response::error(e),
};
let peer_metric = match PeerMetric::from_str(metric_name) {
Ok(m) => m,
Err(e) => return Response::error(e),
};
match hist.peer_query(&addr, peer_metric, window, granularity) {
Some(series) => Response::ok(serde_json::to_value(&series).unwrap_or(Value::Null)),
None => Response::error(format!(
"peer not tracked in stats history: {}",
node.peer_display_name(&addr)
)),
}
} else {
let metric = match Metric::from_str(metric_name) {
Ok(m) => m,
Err(e) => return Response::error(e),
};
let series = hist.query(metric, window, granularity);
Response::ok(serde_json::to_value(&series).unwrap_or(Value::Null))
}
}
fn parse_duration(s: &str) -> Result<Duration, String> {
if s.is_empty() {
return Err("empty duration".to_string());
}
let (num_part, unit) = s.split_at(s.len() - 1);
let n: u64 = num_part
.parse()
.map_err(|_| format!("invalid duration: {s}"))?;
let secs = match unit {
"s" => n,
"m" => n * 60,
"h" => n * 3600,
_ => return Err(format!("unknown duration unit: {unit} (expected s, m, h)")),
};
Ok(Duration::from_secs(secs))
}
pub fn show_stats_all_history(node: &Node, params: Option<&Value>) -> Response {
use Response;
let params = params.cloned().unwrap_or_else(|| json!({}));
let window_str = params
.get("window")
.and_then(|v| v.as_str())
.unwrap_or("10m");
let window = match parse_duration(window_str) {
Ok(d) => d,
Err(e) => return Response::error(e),
};
let granularity_str = params
.get("granularity")
.and_then(|v| v.as_str())
.unwrap_or("1s");
let granularity = match Granularity::from_str(granularity_str) {
Ok(g) => g,
Err(e) => return Response::error(e),
};
let peer_npub = params.get("peer").and_then(|v| v.as_str());
let hist = node.stats_history();
let series: Vec<Value> = if let Some(npub) = peer_npub {
let addr = match parse_peer_npub(npub) {
Ok(a) => a,
Err(e) => return Response::error(e),
};
if !hist.has_peer(&addr) {
return Response::error(format!(
"peer not tracked in stats history: {}",
node.peer_display_name(&addr)
));
}
ALL_PEER_METRICS
.iter()
.map(|m| {
let s = hist
.peer_query(&addr, *m, window, granularity)
.unwrap_or_else(|| {
crate::node::stats_history::Series {
metric: m.name(),
unit: m.unit(),
granularity_seconds: granularity.seconds(),
values: Vec::new(),
}
});
serde_json::to_value(&s).unwrap_or(Value::Null)
})
.collect()
} else {
ALL_METRICS
.iter()
.map(|m| {
let s = hist.query(*m, window, granularity);
serde_json::to_value(&s).unwrap_or(Value::Null)
})
.collect()
};
Response::ok(json!({
"granularity_seconds": granularity.seconds(),
"window_seconds": window.as_secs(),
"peer": peer_npub,
"series": series,
}))
}
pub fn show_stats_peers(node: &Node) -> Value {
let hist = node.stats_history();
let now = std::time::Instant::now();
let mut peers: Vec<Value> = hist
.peers()
.map(|(addr, rings)| {
let last_contact_secs = now.duration_since(rings.last_contact()).as_secs();
let first_seen_secs = now.duration_since(rings.first_seen()).as_secs();
let is_active = node.peers().any(|p| p.node_addr() == addr);
let npub = node
.peers()
.find(|p| p.node_addr() == addr)
.map(|p| p.npub())
.unwrap_or_else(|| hex::encode(addr.as_bytes()));
json!({
"npub": npub,
"node_addr": hex::encode(addr.as_bytes()),
"display_name": node.peer_display_name(addr),
"is_active": is_active,
"first_seen_secs_ago": first_seen_secs,
"last_contact_secs_ago": last_contact_secs,
})
})
.collect();
peers.sort_by(|a, b| {
let a_active = a
.get("is_active")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let b_active = b
.get("is_active")
.and_then(|v| v.as_bool())
.unwrap_or(false);
match (b_active, a_active) {
(true, false) => std::cmp::Ordering::Greater,
(false, true) => std::cmp::Ordering::Less,
_ => a
.get("display_name")
.and_then(|v| v.as_str())
.unwrap_or("")
.cmp(b.get("display_name").and_then(|v| v.as_str()).unwrap_or("")),
}
});
json!({ "peers": peers, "count": peers.len() })
}
pub fn show_stats_history_all_peers(node: &Node, params: Option<&Value>) -> Response {
use Response;
let Some(params) = params else {
return Response::error("missing params for show_stats_history_all_peers");
};
let metric_name = match params.get("metric").and_then(|v| v.as_str()) {
Some(v) => v,
None => return Response::error("missing 'metric' parameter"),
};
let metric = match PeerMetric::from_str(metric_name) {
Ok(m) => m,
Err(e) => return Response::error(e),
};
let window_str = params
.get("window")
.and_then(|v| v.as_str())
.unwrap_or("10m");
let window = match parse_duration(window_str) {
Ok(d) => d,
Err(e) => return Response::error(e),
};
let granularity_str = params
.get("granularity")
.and_then(|v| v.as_str())
.unwrap_or("1s");
let granularity = match Granularity::from_str(granularity_str) {
Ok(g) => g,
Err(e) => return Response::error(e),
};
let hist = node.stats_history();
let peer_addrs: Vec<NodeAddr> = hist.peer_addrs().copied().collect();
let mut peers: Vec<Value> = peer_addrs
.iter()
.filter_map(|addr| {
let s = hist.peer_query(addr, metric, window, granularity)?;
let is_active = node.peers().any(|p| p.node_addr() == addr);
Some(json!({
"node_addr": hex::encode(addr.as_bytes()),
"display_name": node.peer_display_name(addr),
"is_active": is_active,
"values": serde_json::to_value(&s.values).unwrap_or(Value::Null),
}))
})
.collect();
peers.sort_by(|a, b| {
let a_active = a
.get("is_active")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let b_active = b
.get("is_active")
.and_then(|v| v.as_bool())
.unwrap_or(false);
match (b_active, a_active) {
(true, false) => std::cmp::Ordering::Greater,
(false, true) => std::cmp::Ordering::Less,
_ => a
.get("display_name")
.and_then(|v| v.as_str())
.unwrap_or("")
.cmp(b.get("display_name").and_then(|v| v.as_str()).unwrap_or("")),
}
});
Response::ok(json!({
"metric": metric.name(),
"unit": metric.unit(),
"granularity_seconds": granularity.seconds(),
"window_seconds": window.as_secs(),
"peers": peers,
}))
}