use anyhow::{Context, Result};
use serde::Deserialize;
use std::collections::HashMap;
#[derive(Debug, Clone, Default)]
pub struct DashboardImport {
pub title: String,
pub queries: Vec<QueryPanel>,
pub vars: HashMap<String, String>,
pub skipped_panels: usize,
}
#[derive(Debug, Clone)]
pub struct QueryPanel {
pub title: String,
pub exprs: Vec<String>,
pub legends: Vec<Option<String>>, pub grid: Option<GridPos>,
pub panel_type: crate::app::PanelType,
pub thresholds: Option<crate::app::Thresholds>,
pub min: Option<f64>,
pub max: Option<f64>,
}
#[derive(Debug, Clone, Copy)]
pub struct GridPos {
pub x: i32,
pub y: i32,
pub w: i32,
pub h: i32,
}
#[derive(Debug, Deserialize)]
struct RawDashboard {
title: Option<String>,
panels: Option<Vec<RawPanel>>,
templating: Option<RawTemplating>,
}
#[derive(Debug, Deserialize)]
struct RawTemplating {
list: Option<Vec<RawVar>>,
}
#[derive(Debug, Deserialize)]
struct RawVar {
name: String,
current: Option<RawVarCurrent>,
#[serde(rename = "allValue")]
all_value: Option<String>,
}
#[derive(Debug, Deserialize)]
struct RawVarCurrent {
text: Option<serde_json::Value>, value: Option<serde_json::Value>,
}
#[derive(Debug, Deserialize)]
struct RawPanel {
#[serde(rename = "type")]
panel_type: String,
title: Option<String>,
targets: Option<Vec<RawTarget>>,
#[serde(rename = "gridPos")]
grid_pos: Option<RawGridPos>,
panels: Option<Vec<RawPanel>>, #[serde(rename = "fieldConfig")]
field_config: Option<RawFieldConfig>,
}
#[derive(Debug, Deserialize)]
struct RawFieldConfig {
defaults: Option<RawFieldConfigDefaults>,
}
#[derive(Debug, Deserialize)]
struct RawFieldConfigDefaults {
min: Option<f64>,
max: Option<f64>,
thresholds: Option<RawThresholds>,
custom: Option<RawCustom>,
}
#[derive(Debug, Deserialize)]
struct RawCustom {
#[serde(rename = "thresholdsStyle")]
thresholds_style: Option<RawThresholdsStyle>,
}
#[derive(Debug, Deserialize)]
struct RawThresholdsStyle {
mode: Option<String>,
}
#[derive(Debug, Deserialize)]
struct RawThresholds {
mode: Option<String>,
steps: Option<Vec<RawThresholdStep>>,
}
#[derive(Debug, Deserialize)]
struct RawThresholdStep {
value: Option<f64>,
color: Option<String>,
}
#[derive(Debug, Deserialize)]
struct RawTarget {
expr: Option<String>,
#[serde(rename = "legendFormat")]
legend_format: Option<String>,
}
#[derive(Debug, Deserialize)]
struct RawGridPos {
x: i32,
y: i32,
w: i32,
h: i32,
}
pub fn load_grafana_dashboard(path: &std::path::Path) -> Result<DashboardImport> {
let data = std::fs::read_to_string(path)
.with_context(|| format!("reading grafana dashboard: {}", path.display()))?;
let raw: RawDashboard =
serde_json::from_str(&data).with_context(|| "parsing grafana dashboard JSON")?;
let mut vars = HashMap::new();
if let Some(templating) = raw.templating {
if let Some(list) = templating.list {
for v in list {
let val = v
.current
.as_ref()
.and_then(|c| c.value.as_ref())
.or(v.current.as_ref().and_then(|c| c.text.as_ref()));
if let Some(val) = val {
let mut s = match val {
serde_json::Value::String(s) => s.clone(),
serde_json::Value::Array(arr) => {
arr.iter()
.find_map(|x| x.as_str())
.unwrap_or("")
.to_string()
}
serde_json::Value::Number(n) => n.to_string(),
_ => String::new(),
};
if s == "$__all" {
s = v.all_value.clone().unwrap_or_else(|| ".*".to_string());
}
if !s.is_empty() {
vars.insert(v.name, s);
}
}
}
}
}
let mut out = DashboardImport {
title: raw.title.unwrap_or_default(),
queries: vec![],
vars,
skipped_panels: 0,
};
if let Some(panels) = raw.panels {
collect_panels(&mut out, panels)?;
}
Ok(out)
}
fn collect_panels(out: &mut DashboardImport, panels: Vec<RawPanel>) -> Result<()> {
for p in panels.into_iter() {
if let Some(children) = p.panels {
collect_panels(out, children)?;
}
let kind = p.panel_type;
let panel_type = match kind.as_str() {
"graph" | "timeseries" => crate::app::PanelType::Graph,
"stat" => crate::app::PanelType::Stat,
"gauge" => crate::app::PanelType::Gauge,
"bargauge" => crate::app::PanelType::BarGauge,
"table" => crate::app::PanelType::Table,
"heatmap" => crate::app::PanelType::Heatmap,
_ => crate::app::PanelType::Unknown,
};
if panel_type != crate::app::PanelType::Unknown {
let mut exprs = Vec::new();
let mut legends = Vec::new();
for t in p.targets.unwrap_or_default() {
if let Some(e) = t.expr {
exprs.push(e);
legends.push(t.legend_format);
}
}
let mut thresholds = None;
let mut min = None;
let mut max = None;
if let Some(fc) = p.field_config {
if let Some(defaults) = fc.defaults {
min = defaults.min;
max = defaults.max;
if let Some(th) = defaults.thresholds {
let mode = match th.mode.as_deref() {
Some("percentage") => crate::app::ThresholdMode::Percentage,
_ => crate::app::ThresholdMode::Absolute,
};
let mut steps = Vec::new();
if let Some(raw_steps) = th.steps {
for s in raw_steps {
let color = s.color.unwrap_or_else(|| "green".to_string());
let parsed_color = crate::theme::parse_grafana_color(&color);
steps.push(crate::app::ThresholdStep {
value: s.value,
color: parsed_color,
});
}
steps.sort_by(|a, b| {
let a_val = a.value.unwrap_or(f64::NEG_INFINITY);
let b_val = b.value.unwrap_or(f64::NEG_INFINITY);
a_val
.partial_cmp(&b_val)
.unwrap_or(std::cmp::Ordering::Equal)
});
}
if !steps.is_empty() {
let style = defaults
.custom
.and_then(|c| c.thresholds_style)
.and_then(|t| t.mode)
.unwrap_or_else(|| "line".to_string());
thresholds = Some(crate::app::Thresholds {
mode,
steps,
style: Some(style),
});
}
}
}
}
if !exprs.is_empty() {
let gp = p.grid_pos.map(|g| GridPos {
x: g.x,
y: g.y,
w: g.w,
h: g.h,
});
out.queries.push(QueryPanel {
title: p.title.unwrap_or_default(),
exprs,
legends,
grid: gp,
panel_type,
thresholds,
min,
max,
});
}
} else if !kind.is_empty() && kind != "row" {
out.skipped_panels += 1;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_dashboard_vars() {
let json = r#"
{
"title": "Test Dash",
"templating": {
"list": [
{
"name": "job",
"current": { "text": "node-exporter", "value": "node-exporter" }
},
{
"name": "instance",
"current": { "text": "All", "value": ["server1", "server2"] }
}
]
}
}
"#;
let raw: RawDashboard = serde_json::from_str(json).unwrap();
assert_eq!(raw.title.as_deref(), Some("Test Dash"));
let list = raw.templating.unwrap().list.unwrap();
assert_eq!(list.len(), 2);
assert_eq!(list[0].name, "job");
let v = &list[0];
let val = v
.current
.as_ref()
.and_then(|c| c.value.as_ref())
.or(v.current.as_ref().and_then(|c| c.text.as_ref()));
assert_eq!(val.unwrap().as_str(), Some("node-exporter"));
}
}