1use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct Dashboard {
12 pub title: String,
13 pub uid: String,
14 #[serde(rename = "schemaVersion")]
15 pub schema_version: u32,
16 pub version: u32,
17 pub refresh: String,
18 pub time: TimeRange,
19 pub templating: Templating,
20 pub panels: Vec<Panel>,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct TimeRange {
26 pub from: String,
27 pub to: String,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct Templating {
33 pub list: Vec<TemplateVar>,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct TemplateVar {
39 pub name: String,
40 #[serde(rename = "type")]
41 pub var_type: String,
42 pub query: String,
43 pub current: TemplateCurrent,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct TemplateCurrent {
49 pub text: String,
50 pub value: String,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct Panel {
56 pub id: u32,
57 pub title: String,
58 #[serde(rename = "type")]
59 pub panel_type: String,
60 #[serde(skip_serializing_if = "Option::is_none")]
61 pub datasource: Option<String>,
62 pub grid_pos: GridPos,
63 pub targets: Vec<Target>,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct GridPos {
69 pub h: u32,
70 pub w: u32,
71 pub x: u32,
72 pub y: u32,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct Target {
78 pub expr: String,
79 pub legend_format: String,
80 pub ref_id: String,
81}
82
83pub fn generate_dashboard() -> String {
85 let dashboard = Dashboard {
86 title: "Confium Coordinator".into(),
87 uid: "confium-coordinator".into(),
88 schema_version: 39,
89 version: 1,
90 refresh: "10s".into(),
91 time: TimeRange {
92 from: "now-1h".into(),
93 to: "now".into(),
94 },
95 templating: Templating {
96 list: vec![TemplateVar {
97 name: "datasource".into(),
98 var_type: "datasource".into(),
99 query: "Prometheus".into(),
100 current: TemplateCurrent {
101 text: "Prometheus".into(),
102 value: "Prometheus".into(),
103 },
104 }],
105 },
106 panels: vec![
107 Panel {
108 id: 1,
109 title: "Sessions Created (rate)".into(),
110 panel_type: "stat".into(),
111 datasource: Some("$datasource".into()),
112 grid_pos: GridPos { h: 4, w: 6, x: 0, y: 0 },
113 targets: vec![Target {
114 expr: "rate(confium_sessions_created_total[5m])".into(),
115 legend_format: "created/s".into(),
116 ref_id: "A".into(),
117 }],
118 },
119 Panel {
120 id: 2,
121 title: "Active Sessions".into(),
122 panel_type: "gauge".into(),
123 datasource: Some("$datasource".into()),
124 grid_pos: GridPos { h: 4, w: 6, x: 6, y: 0 },
125 targets: vec![Target {
126 expr: "confium_active_sessions".into(),
127 legend_format: "active".into(),
128 ref_id: "A".into(),
129 }],
130 },
131 Panel {
132 id: 3,
133 title: "Registered Signers".into(),
134 panel_type: "stat".into(),
135 datasource: Some("$datasource".into()),
136 grid_pos: GridPos { h: 4, w: 6, x: 12, y: 0 },
137 targets: vec![Target {
138 expr: "confium_registered_signers".into(),
139 legend_format: "signers".into(),
140 ref_id: "A".into(),
141 }],
142 },
143 Panel {
144 id: 4,
145 title: "Session Lifecycle".into(),
146 panel_type: "timeseries".into(),
147 datasource: Some("$datasource".into()),
148 grid_pos: GridPos { h: 8, w: 24, x: 0, y: 4 },
149 targets: vec![
150 Target {
151 expr: "rate(confium_sessions_created_total[5m])".into(),
152 legend_format: "Created".into(),
153 ref_id: "A".into(),
154 },
155 Target {
156 expr: "rate(confium_sessions_completed_total[5m])".into(),
157 legend_format: "Completed".into(),
158 ref_id: "B".into(),
159 },
160 Target {
161 expr: "rate(confium_sessions_expired_total[5m])".into(),
162 legend_format: "Expired".into(),
163 ref_id: "C".into(),
164 },
165 Target {
166 expr: "rate(confium_sessions_aborted_total[5m])".into(),
167 legend_format: "Aborted".into(),
168 ref_id: "D".into(),
169 },
170 ],
171 },
172 Panel {
173 id: 5,
174 title: "Aggregation Success Rate".into(),
175 panel_type: "stat".into(),
176 datasource: Some("$datasource".into()),
177 grid_pos: GridPos { h: 4, w: 8, x: 0, y: 12 },
178 targets: vec![Target {
179 expr: "1 - (rate(confium_aggregations_failed_total[5m]) / clamp_min(rate(confium_aggregations_attempted_total[5m]), 0.001))".into(),
180 legend_format: "success %".into(),
181 ref_id: "A".into(),
182 }],
183 },
184 Panel {
185 id: 6,
186 title: "Bytes Processed (rate)".into(),
187 panel_type: "timeseries".into(),
188 datasource: Some("$datasource".into()),
189 grid_pos: GridPos { h: 8, w: 12, x: 8, y: 12 },
190 targets: vec![Target {
191 expr: "rate(confium_bytes_processed_total[5m])".into(),
192 legend_format: "bytes/s".into(),
193 ref_id: "A".into(),
194 }],
195 },
196 ],
197 };
198 serde_json::to_string_pretty(&dashboard).expect("dashboard serialization")
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 #[test]
206 fn generates_valid_json() {
207 let json = generate_dashboard();
208 assert!(!json.is_empty());
209 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
210 assert!(parsed.is_object());
211 }
212
213 #[test]
214 fn dashboard_has_title() {
215 let json = generate_dashboard();
216 let parsed: Dashboard = serde_json::from_str(&json).unwrap();
217 assert_eq!(parsed.title, "Confium Coordinator");
218 }
219
220 #[test]
221 fn dashboard_has_uid() {
222 let json = generate_dashboard();
223 let parsed: Dashboard = serde_json::from_str(&json).unwrap();
224 assert_eq!(parsed.uid, "confium-coordinator");
225 }
226
227 #[test]
228 fn dashboard_has_6_panels() {
229 let json = generate_dashboard();
230 let parsed: Dashboard = serde_json::from_str(&json).unwrap();
231 assert_eq!(parsed.panels.len(), 6);
232 }
233
234 #[test]
235 fn panels_have_prometheus_targets() {
236 let json = generate_dashboard();
237 let parsed: Dashboard = serde_json::from_str(&json).unwrap();
238 for panel in &parsed.panels {
239 assert!(!panel.targets.is_empty());
240 for target in &panel.targets {
241 assert!(!target.expr.is_empty());
242 assert!(target.expr.contains("confium_"));
243 }
244 }
245 }
246
247 #[test]
248 fn has_datasource_templating() {
249 let json = generate_dashboard();
250 let parsed: Dashboard = serde_json::from_str(&json).unwrap();
251 assert_eq!(parsed.templating.list.len(), 1);
252 assert_eq!(parsed.templating.list[0].name, "datasource");
253 }
254
255 #[test]
256 fn session_lifecycle_panel_has_4_targets() {
257 let json = generate_dashboard();
258 let parsed: Dashboard = serde_json::from_str(&json).unwrap();
259 let lifecycle = parsed.panels.iter().find(|p| p.id == 4).unwrap();
260 assert_eq!(lifecycle.targets.len(), 4);
261 }
262
263 #[test]
264 fn json_is_importable_to_grafana() {
265 let json = generate_dashboard();
266 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
267 assert!(parsed.get("title").is_some());
268 assert!(parsed.get("panels").is_some());
269 assert!(parsed.get("schemaVersion").is_some());
270 assert!(parsed.get("refresh").is_some());
271 }
272}