use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
pub(crate) const CURRENT_GAUGES_PATH: &str = "/data/api/v1/systemPerformance/currentGauges";
pub(crate) const CHARTS_PATH: &str = "/data/api/v1/systemPerformance/charts";
pub(crate) const THREADS_PATH: &str = "/data/api/v1/systemPerformance/threads";
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CurrentGauges {
pub cpu: f64,
#[serde(rename = "heapMemory", serialize_with = "serialize_bytes_f64")]
pub heap_memory: f64,
#[serde(rename = "maxMemory", serialize_with = "serialize_bytes_f64")]
pub max_memory: f64,
#[serde(flatten)]
pub extra: BTreeMap<String, serde_json::Value>,
}
fn serialize_bytes_f64<S>(value: &f64, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
if value.fract() == 0.0 && value.abs() <= 9_007_199_254_740_992.0 {
serializer.serialize_i64(*value as i64)
} else {
serializer.serialize_f64(*value)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ThreadCounts {
#[serde(default)]
pub running: i64,
#[serde(default)]
pub waiting: i64,
#[serde(rename = "timedWaiting", default)]
pub timed_waiting: i64,
#[serde(default)]
pub blocked: i64,
#[serde(flatten)]
pub extra: BTreeMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Datapoint {
#[serde(rename = "histId", default)]
pub hist_id: i64,
pub timestamp: i64,
pub value: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct PerformanceCharts {
#[serde(rename = "cpuChartDatapoints")]
pub cpu_datapoints: Vec<Datapoint>,
#[serde(rename = "heapMemoryDatapoints")]
pub heap_memory_datapoints: Vec<Datapoint>,
#[serde(rename = "nonHeapMemoryDatapoints")]
pub non_heap_memory_datapoints: Vec<Datapoint>,
}
impl<'de> Deserialize<'de> for PerformanceCharts {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct Wire {
#[serde(default, rename = "cpuChartDatapoints")]
cpu: Vec<Datapoint>,
#[serde(default, rename = "memoryChartDatapoints")]
memory: WireMemory,
}
#[derive(Deserialize, Default)]
struct WireMemory {
#[serde(default, rename = "heapMemoryDatapoints")]
heap: Vec<Datapoint>,
#[serde(default, rename = "nonHeapMemoryDatapoints")]
non_heap: Vec<Datapoint>,
}
let wire = Wire::deserialize(deserializer)?;
Ok(Self {
cpu_datapoints: wire.cpu,
heap_memory_datapoints: wire.memory.heap,
non_heap_memory_datapoints: wire.memory.non_heap,
})
}
}
#[cfg(test)]
mod tests {
use super::{CurrentGauges, Datapoint, PerformanceCharts, ThreadCounts};
#[test]
fn current_gauges_parses_the_live_capture() {
let body = serde_json::json!({
"cpu": 4.88,
"heapMemory": 240000000i64,
"maxMemory": 1073741824i64
});
let gauges: CurrentGauges =
serde_json::from_value(body).expect("live gauges shape must parse");
assert!((gauges.cpu - 4.88).abs() < f64::EPSILON, "percent");
assert_eq!(gauges.heap_memory, 240000000.0);
assert_eq!(gauges.max_memory, 1073741824.0);
let round = serde_json::to_value(&gauges).expect("serialize");
assert_eq!(round["cpu"], 4.88, "cpu stays percent");
assert_eq!(round["heapMemory"], 240000000i64, "gateway-native key");
assert_eq!(round["maxMemory"], 1073741824i64);
}
#[test]
fn current_gauges_decodes_exponent_form_java_doubles() {
let raw = r#"{"cpu":1.2755618546264424,"heapMemory":2.85746728E8,"maxMemory":1073741824}"#;
let gauges: CurrentGauges =
serde_json::from_str(raw).expect("8.3.3 exponent-form gauges must parse");
assert_eq!(gauges.heap_memory, 285746728.0);
assert_eq!(gauges.max_memory, 1073741824.0);
let decimal: CurrentGauges = serde_json::from_str(
r#"{"cpu":1.2,"heapMemory":2.8574672E8,"maxMemory":1.073741824E9}"#,
)
.expect("decimal-mantissa exponent form must parse");
assert_eq!(decimal.heap_memory, 285746720.0);
assert_eq!(decimal.max_memory, 1073741824.0);
let round = serde_json::to_value(&gauges).expect("serialize");
assert_eq!(round["heapMemory"], 285746728i64);
assert_eq!(round["maxMemory"], 1073741824i64);
}
#[test]
fn thread_counts_parses_the_live_capture() {
let counts: ThreadCounts = serde_json::from_value(serde_json::json!({
"running": 32, "waiting": 39, "timedWaiting": 51, "blocked": 0
}))
.expect("live threads shape must parse");
assert_eq!(counts.running, 32);
assert_eq!(counts.waiting, 39);
assert_eq!(counts.timed_waiting, 51);
assert_eq!(counts.blocked, 0);
let round = serde_json::to_value(&counts).expect("serialize");
assert_eq!(round["timedWaiting"], 51, "gateway-native key");
}
#[test]
fn charts_parse_nested_wire_and_serialize_flat() {
let wire = serde_json::json!({
"cpuChartDatapoints": [
{"histId": 1, "timestamp": 1787346747022i64, "value": 4.88}
],
"memoryChartDatapoints": {
"heapMemoryDatapoints": [
{"histId": 2, "timestamp": 1787346747022i64, "value": 240000000.0}
],
"nonHeapMemoryDatapoints": [
{"histId": 3, "timestamp": 1787346747022i64, "value": 52000000.0}
]
}
});
let charts: PerformanceCharts =
serde_json::from_value(wire).expect("nested wire shape must parse");
assert_eq!(charts.cpu_datapoints.len(), 1);
assert_eq!(charts.heap_memory_datapoints.len(), 1);
assert_eq!(charts.non_heap_memory_datapoints.len(), 1);
let Datapoint {
hist_id,
timestamp,
value,
} = &charts.cpu_datapoints[0];
assert_eq!((*hist_id, *timestamp), (1, 1787346747022));
assert!((*value - 4.88).abs() < f64::EPSILON);
let flat = serde_json::to_value(&charts).expect("serialize");
assert_eq!(flat["cpuChartDatapoints"][0]["histId"], 1);
assert!(
flat["heapMemoryDatapoints"].is_array() && flat["nonHeapMemoryDatapoints"].is_array(),
"memory series serialize flat under their gateway-native names"
);
}
}