dmsc/observability/
grafana.rs

1//! Copyright © 2025-2026 Wenze Wei. All Rights Reserved.
2//!
3//! This file is part of DMSC.
4//! The DMSC project belongs to the Dunimd Team.
5//!
6//! Licensed under the Apache License, Version 2.0 (the "License");
7//! You may not use this file except in compliance with the License.
8//! You may obtain a copy of the License at
9//!
10//!     http://www.apache.org/licenses/LICENSE-2.0
11//!
12//! Unless required by applicable law or agreed to in writing, software
13//! distributed under the License is distributed on an "AS IS" BASIS,
14//! WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15//! See the License for the specific language governing permissions and
16//! limitations under the License.
17
18#![allow(non_snake_case)]
19
20//! # Grafana Integration Module
21//! 
22//! This module provides data structures for creating and managing Grafana dashboards and panels.
23//! It enables programmatic generation of Grafana dashboards with panels, queries, and layout information.
24//! 
25//! ## Key Components
26//! 
27//! - **DMSCGridPos**: Represents the grid position of a panel on a dashboard
28//! - **DMSCGrafanaPanel**: Represents a single Grafana panel with title, query, type, and position
29//! - **DMSCGrafanaDashboard**: Represents a Grafana dashboard with multiple panels
30//! 
31//! ## Design Principles
32//! 
33//! 1. **Serde Integration**: All structs implement Serialize and Deserialize for easy JSON conversion
34//! 2. **Simple API**: Easy-to-use methods for creating dashboards and adding panels
35//! 3. **Layout Support**: Built-in support for Grafana's grid layout system
36//! 4. **Extensible**: Can be extended to support additional panel types and dashboard features
37//! 5. **Type Safety**: Strongly typed structs for all Grafana components
38//! 6. **JSON Compatibility**: Generates JSON that is compatible with Grafana's API
39//! 
40//! ## Usage
41//! 
42//! ```rust
43//! use dmsc::prelude::*;
44//! 
45//! fn example() -> DMSCResult<()> {
46//!     // Create a new dashboard
47//!     let mut dashboard = DMSCGrafanaDashboard::new("DMSC Metrics");
48//!     
49//!     // Create a panel
50//!     let panel = DMSCGrafanaPanel {
51//!         title: "Request Rate".to_string(),
52//!         query: "rate(http_requests_total[5m])".to_string(),
53//!         panel_type: "graph".to_string(),
54//!         grid_pos: DMSCGridPos {
55//!             h: 8,
56//!             w: 12,
57//!             x: 0,
58//!             y: 0,
59//!         },
60//!     };
61//!     
62//!     // Add panel to dashboard
63//!     dashboard.add_panel(panel)?;
64//!     
65//!     // Convert to JSON for Grafana API
66//!     let json = dashboard.to_json()?;
67//!     println!("Dashboard JSON: {}", json);
68//!     
69//!     Ok(())
70//! }
71//! ```
72
73use serde::{Serialize, Deserialize};
74use crate::core::DMSCResult;
75
76/// Grafana target configuration for data sources
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct DMSCGrafanaTarget {
79    pub expr: String,
80    pub ref_id: String,
81    pub legend_format: Option<String>,
82    pub interval: Option<String>,
83}
84
85/// Grafana grid position configuration
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct DMSCGridPos {
88    pub h: i32,
89    pub w: i32,
90    pub x: i32,
91    pub y: i32,
92}
93
94/// Grafana panel configuration
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct DMSCGrafanaPanel {
97    pub id: i32,
98    pub title: String,
99    pub type_: String,
100    pub targets: Vec<DMSCGrafanaTarget>,
101    pub grid_pos: DMSCGridPos,
102    pub field_config: serde_json::Value,
103    pub options: serde_json::Value,
104    pub description: Option<String>,
105    pub datasource: Option<String>,
106}
107
108/// Grafana time range configuration
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct DMSCGrafanaTimeRange {
111    pub from: String,
112    pub to: String,
113}
114
115/// Grafana dashboard tag
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct DMSCGrafanaTag {
118    pub term: String,
119    pub color: Option<String>,
120}
121
122/// Grafana dashboard configuration
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct DMSCGrafanaDashboard {
125    pub title: String,
126    pub panels: Vec<DMSCGrafanaPanel>,
127    pub tags: Vec<DMSCGrafanaTag>,
128    pub time: DMSCGrafanaTimeRange,
129    pub refresh: String,
130    pub timezone: String,
131    pub schema_version: i32,
132    pub uid: Option<String>,
133    pub version: i32,
134}
135
136/// Grafana dashboard generator
137pub struct DMSCGrafanaDashboardGenerator {
138    next_panel_id: i32,
139}
140
141#[allow(dead_code)]
142impl DMSCGrafanaDashboard {
143    pub fn new(title: &str) -> Self {
144        DMSCGrafanaDashboard {
145            title: title.to_string(),
146            panels: Vec::new(),
147            tags: vec![DMSCGrafanaTag { term: "dms".to_string(), color: Some("#1F77B4".to_string()) }],
148            time: DMSCGrafanaTimeRange { from: "now-1h".to_string(), to: "now".to_string() },
149            refresh: "5s".to_string(),
150            timezone: "browser".to_string(),
151            schema_version: 38,
152            uid: None,
153            version: 1,
154        }
155    }
156    
157    pub fn add_panel(&mut self, panel: DMSCGrafanaPanel) -> DMSCResult<()> {
158        self.panels.push(panel);
159        Ok(())
160    }
161    
162    pub fn to_json(&self) -> DMSCResult<String> {
163        serde_json::to_string(self).map_err(|e| crate::core::DMSCError::Serde(e.to_string()))
164    }
165}
166
167impl DMSCGrafanaDashboardGenerator {
168    pub fn new() -> Self {
169        DMSCGrafanaDashboardGenerator {
170            next_panel_id: 1,
171        }
172    }
173    
174    /// Convert a string to title case (first letter of each word uppercase)
175    fn title_case(&self, s: &str) -> String {
176        let mut result = String::new();
177        let mut capitalize_next = true;
178        
179        for c in s.chars() {
180            if c == '_' || c == ' ' {
181                result.push(' ');
182                capitalize_next = true;
183            } else if capitalize_next {
184                result.push(c.to_ascii_uppercase());
185                capitalize_next = false;
186            } else {
187                result.push(c.to_ascii_lowercase());
188            }
189        }
190        
191        result
192    }
193    
194    /// Create a new dashboard with default settings
195    pub fn create_dashboard(&self, title: &str) -> DMSCGrafanaDashboard {
196        DMSCGrafanaDashboard::new(title)
197    }
198    
199    /// Create a Prometheus target
200    pub fn create_prometheus_target(&self, expr: &str, ref_id: &str, legend_format: Option<&str>) -> DMSCGrafanaTarget {
201        DMSCGrafanaTarget {
202            expr: expr.to_string(),
203            ref_id: ref_id.to_string(),
204            legend_format: legend_format.map(|s| s.to_string()),
205            interval: None,
206        }
207    }
208    
209    /// Create a new panel with default settings
210    pub fn create_panel(&mut self, title: &str, panel_type: &str, x: i32, y: i32, w: i32, h: i32) -> DMSCGrafanaPanel {
211        let panel_id = self.next_panel_id;
212        self.next_panel_id += 1;
213        
214        DMSCGrafanaPanel {
215            id: panel_id,
216            title: title.to_string(),
217            type_: panel_type.to_string(),
218            targets: Vec::new(),
219            grid_pos: DMSCGridPos { h, w, x, y },
220            field_config: serde_json::json!({ "defaults": {}, "overrides": [] }),
221            options: serde_json::json!({}),
222            description: None,
223            datasource: Some("Prometheus".to_string()),
224        }
225    }
226    
227    /// Create a graph panel for request rate
228    pub fn create_request_rate_panel(&mut self, x: i32, y: i32, w: i32, h: i32) -> DMSCGrafanaPanel {
229        let mut panel = self.create_panel("Request Rate", "timeseries", x, y, w, h);
230        panel.targets.push(self.create_prometheus_target(
231            "rate(dms_requests_total[5m])",
232            "A",
233            Some("{{instance}}")
234        ));
235        panel
236    }
237    
238    /// Create a graph panel for request duration
239    pub fn create_request_duration_panel(&mut self, x: i32, y: i32, w: i32, h: i32) -> DMSCGrafanaPanel {
240        let mut panel = self.create_panel("Request Duration", "timeseries", x, y, w, h);
241        panel.targets.push(self.create_prometheus_target(
242            "histogram_quantile(0.95, sum(rate(dms_request_duration_seconds_bucket[5m])) by (le))",
243            "A",
244            Some("95th Percentile")
245        ));
246        panel.targets.push(self.create_prometheus_target(
247            "histogram_quantile(0.5, sum(rate(dms_request_duration_seconds_bucket[5m])) by (le))",
248            "B",
249            Some("50th Percentile")
250        ));
251        panel
252    }
253    
254    /// Create a stat panel for active connections
255    pub fn create_active_connections_panel(&mut self, x: i32, y: i32, w: i32, h: i32) -> DMSCGrafanaPanel {
256        let mut panel = self.create_panel("Active Connections", "stat", x, y, w, h);
257        panel.targets.push(self.create_prometheus_target(
258            "dms_active_connections",
259            "A",
260            None
261        ));
262        panel
263    }
264    
265    /// Create a graph panel for error rate
266    pub fn create_error_rate_panel(&mut self, x: i32, y: i32, w: i32, h: i32) -> DMSCGrafanaPanel {
267        let mut panel = self.create_panel("Error Rate", "timeseries", x, y, w, h);
268        panel.targets.push(self.create_prometheus_target(
269            "rate(dms_errors_total[5m])",
270            "A",
271            Some("{{instance}}")
272        ));
273        panel
274    }
275    
276    /// Create a graph panel for cache metrics
277    pub fn create_cache_metrics_panel(&mut self, x: i32, y: i32, w: i32, h: i32) -> DMSCGrafanaPanel {
278        let mut panel = self.create_panel("Cache Metrics", "timeseries", x, y, w, h);
279        panel.targets.push(self.create_prometheus_target(
280            "rate(dms_cache_hits_total[5m])",
281            "A",
282            Some("Hits")
283        ));
284        panel.targets.push(self.create_prometheus_target(
285            "rate(dms_cache_misses_total[5m])",
286            "B",
287            Some("Misses")
288        ));
289        panel
290    }
291    
292    /// Create a graph panel for database query time
293    pub fn create_db_query_time_panel(&mut self, x: i32, y: i32, w: i32, h: i32) -> DMSCGrafanaPanel {
294        let mut panel = self.create_panel("Database Query Time", "timeseries", x, y, w, h);
295        panel.targets.push(self.create_prometheus_target(
296            "histogram_quantile(0.95, sum(rate(dms_db_query_duration_seconds_bucket[5m])) by (le))",
297            "A",
298            Some("95th Percentile")
299        ));
300        panel
301    }
302    
303    /// Generate a default DMSC dashboard with common metrics panels
304    pub fn generate_default_dashboard(&mut self) -> DMSCResult<DMSCGrafanaDashboard> {
305        let mut dashboard = self.create_dashboard("DMSC Default Dashboard");
306        
307        // First row: Request metrics
308        dashboard.add_panel(self.create_request_rate_panel(0, 0, 12, 8))?;
309        dashboard.add_panel(self.create_request_duration_panel(12, 0, 12, 8))?;
310        
311        // Second row: Error and connection metrics
312        dashboard.add_panel(self.create_error_rate_panel(0, 8, 12, 8))?;
313        dashboard.add_panel(self.create_active_connections_panel(12, 8, 6, 8))?;
314        
315        // Third row: Cache and database metrics
316        dashboard.add_panel(self.create_cache_metrics_panel(0, 16, 12, 8))?;
317        dashboard.add_panel(self.create_db_query_time_panel(12, 16, 12, 8))?;
318        
319        Ok(dashboard)
320    }
321    
322    /// Generate a dashboard automatically based on available metrics
323    /// 
324    /// This method analyzes available metrics and generates an appropriate dashboard
325    /// with panels matching the metric types and values.
326    /// 
327    /// # Parameters
328    /// 
329    /// - `metrics`: List of available metric names
330    /// - `dashboard_title`: Title for the generated dashboard
331    /// 
332    /// # Returns
333    /// 
334    /// A Grafana dashboard automatically generated based on the provided metrics
335    pub fn generate_auto_dashboard(&mut self, metrics: Vec<&str>, dashboard_title: &str) -> DMSCResult<DMSCGrafanaDashboard> {
336        let mut dashboard = self.create_dashboard(dashboard_title);
337        
338        // Analyze metrics and group by type
339        let mut counter_metrics = Vec::new();
340        let mut gauge_metrics = Vec::new();
341        let mut histogram_metrics = Vec::new();
342        
343        for metric in metrics {
344            if metric.ends_with("_total") || metric.contains("count") {
345                counter_metrics.push(metric);
346            } else if metric.ends_with("_seconds") || metric.ends_with("_bytes") || metric.contains("time") {
347                histogram_metrics.push(metric);
348            } else {
349                gauge_metrics.push(metric);
350            }
351        }
352        
353        // Generate panels based on metric types
354        let mut current_row = 0;
355        
356        // Add counter panels
357        for (i, metric) in counter_metrics.iter().enumerate() {
358            let panel = self.create_counter_panel(*metric, i as i32 * 12, current_row, 12, 8);
359            dashboard.add_panel(panel)?;
360            if (i + 1) % 2 == 0 {
361                current_row += 8;
362            }
363        }
364        
365        if counter_metrics.len() % 2 != 0 {
366            current_row += 8;
367        }
368        
369        // Add gauge panels
370        for (i, metric) in gauge_metrics.iter().enumerate() {
371            let panel = self.create_gauge_panel(*metric, i as i32 * 12, current_row, 12, 8);
372            dashboard.add_panel(panel)?;
373            if (i + 1) % 2 == 0 {
374                current_row += 8;
375            }
376        }
377        
378        if gauge_metrics.len() % 2 != 0 {
379            current_row += 8;
380        }
381        
382        // Add histogram panels
383        for (i, metric) in histogram_metrics.iter().enumerate() {
384            let panel = self.create_histogram_panel(*metric, i as i32 * 12, current_row, 12, 8);
385            dashboard.add_panel(panel)?;
386            if (i + 1) % 2 == 0 {
387                current_row += 8;
388            }
389        }
390        
391        Ok(dashboard)
392    }
393    
394    /// Create a counter panel for a metric
395    pub fn create_counter_panel(&mut self, metric_name: &str, x: i32, y: i32, w: i32, h: i32) -> DMSCGrafanaPanel {
396        let title = self.title_case(metric_name);
397        let query = format!("rate({}[5m])", metric_name);
398        
399        let mut panel = self.create_panel(&title, "timeseries", x, y, w, h);
400        panel.targets.push(self.create_prometheus_target(&query, "A", Some("{{instance}}")));
401        panel
402    }
403    
404    /// Create a gauge panel for a metric
405    pub fn create_gauge_panel(&mut self, metric_name: &str, x: i32, y: i32, w: i32, h: i32) -> DMSCGrafanaPanel {
406        let title = self.title_case(metric_name);
407        
408        let mut panel = self.create_panel(&title, "stat", x, y, w, h);
409        panel.targets.push(self.create_prometheus_target(metric_name, "A", None));
410        panel
411    }
412    
413    /// Create a histogram panel for a metric
414    pub fn create_histogram_panel(&mut self, metric_name: &str, x: i32, y: i32, w: i32, h: i32) -> DMSCGrafanaPanel {
415        let title = self.title_case(metric_name);
416        let query_95 = format!("histogram_quantile(0.95, sum(rate({}_bucket[5m])) by (le))", metric_name);
417        let query_50 = format!("histogram_quantile(0.5, sum(rate({}_bucket[5m])) by (le))", metric_name);
418        
419        let mut panel = self.create_panel(&title, "timeseries", x, y, w, h);
420        panel.targets.push(self.create_prometheus_target(&query_95, "A", Some("95th Percentile")));
421        panel.targets.push(self.create_prometheus_target(&query_50, "B", Some("50th Percentile")));
422        panel
423    }
424}