use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GrafanaDashboard {
pub title: String,
pub uid: String,
pub version: i32,
pub description: String,
pub tags: Vec<String>,
pub refresh: String,
pub time: TimeRange,
pub panels: Vec<serde_json::Value>,
pub templating: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimeRange {
pub from: String,
pub to: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GrafanaDataSource {
pub name: String,
pub r#type: String,
pub url: String,
pub access: String,
pub is_default: bool,
}
pub struct GrafanaManager {
dashboards_dir: String,
}
impl GrafanaManager {
pub fn new(dashboards_dir: Option<String>) -> Self {
Self {
dashboards_dir: dashboards_dir.unwrap_or_else(|| "dashboards".to_string()),
}
}
pub fn load_dashboard(&self, dashboard_name: &str) -> Result<GrafanaDashboard> {
let dashboard_path = Path::new(&self.dashboards_dir).join(format!("{dashboard_name}.json"));
if !dashboard_path.exists() {
return Err(anyhow::anyhow!(
"Dashboard file not found: {}",
dashboard_path.display()
));
}
let content = fs::read_to_string(&dashboard_path)
.map_err(|e| anyhow::anyhow!("Failed to read dashboard file: {}", e))?;
let dashboard: GrafanaDashboard = serde_json::from_str(&content)
.map_err(|e| anyhow::anyhow!("Failed to parse dashboard JSON: {}", e))?;
Ok(dashboard)
}
pub fn list_dashboards(&self) -> Result<Vec<String>> {
let dashboards_path = Path::new(&self.dashboards_dir);
if !dashboards_path.exists() {
return Ok(Vec::new());
}
let mut dashboards = Vec::new();
for entry in fs::read_dir(dashboards_path)? {
let entry = entry?;
let path = entry.path();
if path.is_file() && path.extension().is_some_and(|ext| ext == "json") {
if let Some(name) = path.file_stem().and_then(|s| s.to_str()) {
dashboards.push(name.to_string());
}
}
}
dashboards.sort();
Ok(dashboards)
}
pub fn get_dashboard_info(&self, dashboard_name: &str) -> Result<DashboardInfo> {
let dashboard = self.load_dashboard(dashboard_name)?;
Ok(DashboardInfo {
name: dashboard_name.to_string(),
title: dashboard.title,
uid: dashboard.uid,
description: dashboard.description,
tags: dashboard.tags,
panel_count: dashboard.panels.len(),
refresh_rate: dashboard.refresh,
time_range: dashboard.time,
})
}
pub fn create_prometheus_datasource(&self, prometheus_url: &str) -> GrafanaDataSource {
GrafanaDataSource {
name: "Pulzr Prometheus".to_string(),
r#type: "prometheus".to_string(),
url: prometheus_url.to_string(),
access: "proxy".to_string(),
is_default: true,
}
}
pub fn generate_import_instructions(&self, dashboard_name: &str) -> Result<String> {
let info = self.get_dashboard_info(dashboard_name)?;
#[allow(clippy::format_in_format_args)]
let instructions = format!(
r#"# Import Instructions for {dashboard_name}
## Dashboard Information
- **Title**: {title}
- **UID**: {uid}
- **Description**: {description}
- **Tags**: {tags}
- **Panels**: {panel_count}
- **Refresh Rate**: {refresh_rate}
## Steps to Import
1. **Enable Prometheus in Pulzr**:
```bash
pulzr --url https://httpbin.org/get --prometheus --prometheus-port 9090
```
2. **Configure Prometheus Data Source**:
- Go to Configuration → Data Sources in Grafana
- Add new Prometheus data source
- URL: `http://localhost:9090`
- Save & Test
3. **Import Dashboard**:
- Go to Create → Import in Grafana
- Upload: `{dashboard_file}`
- Select Prometheus data source
- Save
## Dashboard Features
- Real-time metrics with {refresh_rate} refresh
- Time range: {time_from} to {time_to}
- {panel_count} visualization panels
- Optimized for load testing analysis
## Required Metrics
This dashboard requires the following Pulzr metrics:
- pulzr_requests_total
- pulzr_requests_successful_total
- pulzr_requests_failed_total
- pulzr_requests_per_second
- pulzr_response_time_p*_ms
- pulzr_request_duration_seconds
- pulzr_active_connections
- pulzr_bytes_received_total
- pulzr_error_rate_percent
## Troubleshooting
- Verify Prometheus endpoint is accessible
- Check that Pulzr is running with --prometheus flag
- Confirm data source configuration in Grafana
"#,
dashboard_name = dashboard_name,
title = info.title,
uid = info.uid,
description = info.description,
tags = info.tags.join(", "),
panel_count = info.panel_count,
refresh_rate = info.refresh_rate,
dashboard_file = format!("{}.json", dashboard_name),
time_from = info.time_range.from,
time_to = info.time_range.to,
);
Ok(instructions)
}
pub fn validate_dashboard(&self, dashboard_name: &str) -> Result<Vec<String>> {
let dashboard = self.load_dashboard(dashboard_name)?;
let mut issues = Vec::new();
if dashboard.title.is_empty() {
issues.push("Dashboard title is empty".to_string());
}
if dashboard.uid.is_empty() {
issues.push("Dashboard UID is empty".to_string());
}
if dashboard.panels.is_empty() {
issues.push("Dashboard has no panels".to_string());
}
let dashboard_str = serde_json::to_string(&dashboard)?;
if !dashboard_str.contains("pulzr_") {
issues.push("Dashboard doesn't seem to contain Pulzr metrics".to_string());
}
if dashboard.time.from.is_empty() || dashboard.time.to.is_empty() {
issues.push("Time range is not properly configured".to_string());
}
Ok(issues)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DashboardInfo {
pub name: String,
pub title: String,
pub uid: String,
pub description: String,
pub tags: Vec<String>,
pub panel_count: usize,
pub refresh_rate: String,
pub time_range: TimeRange,
}
impl DashboardInfo {
pub fn print_summary(&self) {
println!("📊 Dashboard: {}", self.name);
println!(" Title: {}", self.title);
println!(" UID: {}", self.uid);
println!(" Description: {}", self.description);
println!(" Tags: {}", self.tags.join(", "));
println!(" Panels: {}", self.panel_count);
println!(" Refresh: {}", self.refresh_rate);
println!(
" Time Range: {} to {}",
self.time_range.from, self.time_range.to
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_grafana_manager_creation() {
let manager = GrafanaManager::new(None);
assert_eq!(manager.dashboards_dir, "dashboards");
let manager = GrafanaManager::new(Some("custom_dir".to_string()));
assert_eq!(manager.dashboards_dir, "custom_dir");
}
#[test]
fn test_create_prometheus_datasource() {
let manager = GrafanaManager::new(None);
let datasource = manager.create_prometheus_datasource("http://localhost:9090");
assert_eq!(datasource.name, "Pulzr Prometheus");
assert_eq!(datasource.r#type, "prometheus");
assert_eq!(datasource.url, "http://localhost:9090");
assert_eq!(datasource.access, "proxy");
assert!(datasource.is_default);
}
#[test]
fn test_dashboard_validation() {
let temp_dir = TempDir::new().unwrap();
let dashboards_dir = temp_dir.path().join("dashboards");
fs::create_dir_all(&dashboards_dir).unwrap();
let dashboard_content = r#"{
"title": "Test Dashboard",
"uid": "test-dashboard",
"version": 1,
"description": "Test dashboard for validation",
"tags": ["test"],
"refresh": "5s",
"time": {
"from": "now-5m",
"to": "now"
},
"panels": [
{
"id": 1,
"title": "Test Panel",
"type": "timeseries",
"targets": [
{
"expr": "pulzr_requests_total"
}
]
}
],
"templating": {
"list": []
}
}"#;
let dashboard_path = dashboards_dir.join("test-dashboard.json");
fs::write(&dashboard_path, dashboard_content).unwrap();
let manager = GrafanaManager::new(Some(dashboards_dir.to_string_lossy().to_string()));
let issues = manager.validate_dashboard("test-dashboard").unwrap();
assert!(issues.is_empty(), "Dashboard should be valid: {issues:?}");
}
#[test]
fn test_list_dashboards() {
let temp_dir = TempDir::new().unwrap();
let dashboards_dir = temp_dir.path().join("dashboards");
fs::create_dir_all(&dashboards_dir).unwrap();
fs::write(dashboards_dir.join("dashboard1.json"), "{}").unwrap();
fs::write(dashboards_dir.join("dashboard2.json"), "{}").unwrap();
fs::write(dashboards_dir.join("not-a-dashboard.txt"), "{}").unwrap();
let manager = GrafanaManager::new(Some(dashboards_dir.to_string_lossy().to_string()));
let dashboards = manager.list_dashboards().unwrap();
assert_eq!(dashboards.len(), 2);
assert!(dashboards.contains(&"dashboard1".to_string()));
assert!(dashboards.contains(&"dashboard2".to_string()));
assert!(!dashboards.contains(&"not-a-dashboard".to_string()));
}
}