1use std::collections::BTreeMap;
16
17use serde::{Deserialize, Serialize};
18
19pub(crate) const CURRENT_GAUGES_PATH: &str = "/data/api/v1/systemPerformance/currentGauges";
21pub(crate) const CHARTS_PATH: &str = "/data/api/v1/systemPerformance/charts";
23pub(crate) const THREADS_PATH: &str = "/data/api/v1/systemPerformance/threads";
25
26#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
43pub struct CurrentGauges {
44 pub cpu: f64,
47 #[serde(rename = "heapMemory", serialize_with = "serialize_bytes_f64")]
50 pub heap_memory: f64,
51 #[serde(rename = "maxMemory", serialize_with = "serialize_bytes_f64")]
53 pub max_memory: f64,
54 #[serde(flatten)]
56 pub extra: BTreeMap<String, serde_json::Value>,
57}
58
59fn serialize_bytes_f64<S>(value: &f64, serializer: S) -> Result<S::Ok, S::Error>
66where
67 S: serde::Serializer,
68{
69 if value.fract() == 0.0 && value.abs() <= 9_007_199_254_740_992.0 {
70 serializer.serialize_i64(*value as i64)
71 } else {
72 serializer.serialize_f64(*value)
73 }
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
79pub struct ThreadCounts {
80 #[serde(default)]
82 pub running: i64,
83 #[serde(default)]
85 pub waiting: i64,
86 #[serde(rename = "timedWaiting", default)]
88 pub timed_waiting: i64,
89 #[serde(default)]
91 pub blocked: i64,
92 #[serde(flatten)]
94 pub extra: BTreeMap<String, serde_json::Value>,
95}
96
97#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
99pub struct Datapoint {
100 #[serde(rename = "histId", default)]
102 pub hist_id: i64,
103 pub timestamp: i64,
105 pub value: f64,
107}
108
109#[derive(Debug, Clone, PartialEq, Serialize)]
120pub struct PerformanceCharts {
121 #[serde(rename = "cpuChartDatapoints")]
123 pub cpu_datapoints: Vec<Datapoint>,
124 #[serde(rename = "heapMemoryDatapoints")]
126 pub heap_memory_datapoints: Vec<Datapoint>,
127 #[serde(rename = "nonHeapMemoryDatapoints")]
129 pub non_heap_memory_datapoints: Vec<Datapoint>,
130}
131
132impl<'de> Deserialize<'de> for PerformanceCharts {
133 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
134 where
135 D: serde::Deserializer<'de>,
136 {
137 #[derive(Deserialize)]
139 struct Wire {
140 #[serde(default, rename = "cpuChartDatapoints")]
141 cpu: Vec<Datapoint>,
142 #[serde(default, rename = "memoryChartDatapoints")]
143 memory: WireMemory,
144 }
145 #[derive(Deserialize, Default)]
146 struct WireMemory {
147 #[serde(default, rename = "heapMemoryDatapoints")]
148 heap: Vec<Datapoint>,
149 #[serde(default, rename = "nonHeapMemoryDatapoints")]
150 non_heap: Vec<Datapoint>,
151 }
152 let wire = Wire::deserialize(deserializer)?;
153 Ok(Self {
154 cpu_datapoints: wire.cpu,
155 heap_memory_datapoints: wire.memory.heap,
156 non_heap_memory_datapoints: wire.memory.non_heap,
157 })
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::{CurrentGauges, Datapoint, PerformanceCharts, ThreadCounts};
164
165 #[test]
168 fn current_gauges_parses_the_live_capture() {
169 let body = serde_json::json!({
170 "cpu": 4.88,
171 "heapMemory": 240000000i64,
172 "maxMemory": 1073741824i64
173 });
174 let gauges: CurrentGauges =
175 serde_json::from_value(body).expect("live gauges shape must parse");
176 assert!((gauges.cpu - 4.88).abs() < f64::EPSILON, "percent");
177 assert_eq!(gauges.heap_memory, 240000000.0);
178 assert_eq!(gauges.max_memory, 1073741824.0);
179
180 let round = serde_json::to_value(&gauges).expect("serialize");
181 assert_eq!(round["cpu"], 4.88, "cpu stays percent");
182 assert_eq!(round["heapMemory"], 240000000i64, "gateway-native key");
183 assert_eq!(round["maxMemory"], 1073741824i64);
184 }
185
186 #[test]
194 fn current_gauges_decodes_exponent_form_java_doubles() {
195 let raw = r#"{"cpu":1.2755618546264424,"heapMemory":2.85746728E8,"maxMemory":1073741824}"#;
196 let gauges: CurrentGauges =
197 serde_json::from_str(raw).expect("8.3.3 exponent-form gauges must parse");
198 assert_eq!(gauges.heap_memory, 285746728.0);
199 assert_eq!(gauges.max_memory, 1073741824.0);
200
201 let decimal: CurrentGauges = serde_json::from_str(
203 r#"{"cpu":1.2,"heapMemory":2.8574672E8,"maxMemory":1.073741824E9}"#,
204 )
205 .expect("decimal-mantissa exponent form must parse");
206 assert_eq!(decimal.heap_memory, 285746720.0);
207 assert_eq!(decimal.max_memory, 1073741824.0);
208
209 let round = serde_json::to_value(&gauges).expect("serialize");
212 assert_eq!(round["heapMemory"], 285746728i64);
213 assert_eq!(round["maxMemory"], 1073741824i64);
214 }
215
216 #[test]
219 fn thread_counts_parses_the_live_capture() {
220 let counts: ThreadCounts = serde_json::from_value(serde_json::json!({
221 "running": 32, "waiting": 39, "timedWaiting": 51, "blocked": 0
222 }))
223 .expect("live threads shape must parse");
224 assert_eq!(counts.running, 32);
225 assert_eq!(counts.waiting, 39);
226 assert_eq!(counts.timed_waiting, 51);
227 assert_eq!(counts.blocked, 0);
228
229 let round = serde_json::to_value(&counts).expect("serialize");
230 assert_eq!(round["timedWaiting"], 51, "gateway-native key");
231 }
232
233 #[test]
237 fn charts_parse_nested_wire_and_serialize_flat() {
238 let wire = serde_json::json!({
239 "cpuChartDatapoints": [
240 {"histId": 1, "timestamp": 1787346747022i64, "value": 4.88}
241 ],
242 "memoryChartDatapoints": {
243 "heapMemoryDatapoints": [
244 {"histId": 2, "timestamp": 1787346747022i64, "value": 240000000.0}
245 ],
246 "nonHeapMemoryDatapoints": [
247 {"histId": 3, "timestamp": 1787346747022i64, "value": 52000000.0}
248 ]
249 }
250 });
251 let charts: PerformanceCharts =
252 serde_json::from_value(wire).expect("nested wire shape must parse");
253 assert_eq!(charts.cpu_datapoints.len(), 1);
254 assert_eq!(charts.heap_memory_datapoints.len(), 1);
255 assert_eq!(charts.non_heap_memory_datapoints.len(), 1);
256 let Datapoint {
257 hist_id,
258 timestamp,
259 value,
260 } = &charts.cpu_datapoints[0];
261 assert_eq!((*hist_id, *timestamp), (1, 1787346747022));
262 assert!((*value - 4.88).abs() < f64::EPSILON);
263
264 let flat = serde_json::to_value(&charts).expect("serialize");
265 assert_eq!(flat["cpuChartDatapoints"][0]["histId"], 1);
266 assert!(
267 flat["heapMemoryDatapoints"].is_array() && flat["nonHeapMemoryDatapoints"].is_array(),
268 "memory series serialize flat under their gateway-native names"
269 );
270 }
271}