Skip to main content

confium_coordinator/coordinator/
grafana.rs

1//! Grafana dashboard JSON generation for coordinator metrics.
2//!
3//! Produces a Grafana dashboard JSON that visualizes the Prometheus
4//! metrics exported by the coordinator. Import directly via the
5//! Grafana UI (Dashboards → Import → paste JSON).
6
7use serde::{Deserialize, Serialize};
8
9/// A Grafana dashboard.
10#[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/// Time range for the dashboard.
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct TimeRange {
26    pub from: String,
27    pub to: String,
28}
29
30/// Templating variables.
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct Templating {
33    pub list: Vec<TemplateVar>,
34}
35
36/// A template variable.
37#[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/// Current value for a template variable.
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct TemplateCurrent {
49    pub text: String,
50    pub value: String,
51}
52
53/// A dashboard panel.
54#[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/// Grid position.
67#[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/// A Prometheus query target.
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct Target {
78    pub expr: String,
79    pub legend_format: String,
80    pub ref_id: String,
81}
82
83/// Generate the default Confium coordinator dashboard JSON.
84pub 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}