diff --git a/examples/example_bad_dashboard.yaml b/examples/example_bad_dashboard.yaml new file mode 100644 index 0000000..79abd9e --- /dev/null +++ b/examples/example_bad_dashboard.yaml @@ -0,0 +1,18 @@ +--- # A list of dashboards +- title: Invalid Dasbboard + graphs: # Each Dashboard can have 1 or more graphs in it. + - title: Node cpu # Graphs have titles + query_type: Range # The type of graph. Range for timeseries and Scalar for point in time + d3_tick_format: "~s" # Default tick format for the graph y axis + plots: # List of pluts to show on the graph + - source: http://heimdall:9001 # Prometheus source uri for this plot + query: 'sum by (instance)(irate(node_cpu_seconds_total{job="nodestats"}[5m])' # syntax error in query + meta: # metadata for this plot + name_format: "`${labels.instance}`" # javascript template literal to format the trace name + fill: tozeroy + #d3_tick_format: "~%" # d3 tick format override for this plot's yaxis + #named_axis: "y" # yaxis name to use for this subplots traces + span: # The span for this range query + end: now # Where the span ends. RFC3339 format with special handling for the now keyword + duration: 1d # duration of the span. Uses SI formatting for duration amounts. + step_duration: 10min # step size for the duration amounts. diff --git a/src/dashboard.rs b/src/dashboard.rs index db43b4a..3d85f58 100644 --- a/src/dashboard.rs +++ b/src/dashboard.rs @@ -18,8 +18,9 @@ use chrono::Duration; use serde::Deserialize; use serde_yaml; use tracing::{debug, error}; +use anyhow::Result; -use crate::query::{QueryConn, QueryType, PlotMeta}; +use crate::query::{QueryConn, QueryType, QueryResult, PlotMeta, to_samples}; #[derive(Deserialize, Debug)] pub struct GraphSpan { @@ -52,6 +53,21 @@ pub struct Graph { pub d3_tick_format: Option, } +pub async fn query_data(graph: &Graph, dash: &Dashboard, query_span: Option) -> Result> { + let connections = graph.get_query_connections(&dash.span, &query_span); + let mut data = Vec::new(); + for conn in connections { + data.push(to_samples( + conn.get_results() + .await? + .data() + .clone(), + conn.meta, + )); + } + Ok(data) +} + fn duration_from_string(duration_string: &str) -> Option { match parse_duration::parse(duration_string) { Ok(d) => match Duration::from_std(d) { diff --git a/src/main.rs b/src/main.rs index cdb66b6..cea5309 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,12 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. use std::path::PathBuf; - use anyhow; use axum::{self, extract::State, routing::*, Router}; use clap::{self, Parser, ValueEnum}; +use dashboard::{Dashboard, query_data}; use tokio::net::TcpListener; use tower_http::trace::TraceLayer; +use tracing::{error, info}; use tracing::Level; use tracing_subscriber::FmtSubscriber; @@ -43,6 +44,19 @@ struct Cli { pub config: PathBuf, #[arg(long, value_enum, default_value_t = Verbosity::INFO)] pub verbose: Verbosity, + #[arg(long, default_value_t = false)] + pub validate: bool, +} + +async fn validate(dash: &Dashboard) -> anyhow::Result<()> { + for graph in dash.graphs.iter() { + let data = query_data(graph, &dash, None).await; + if data.is_err() { + error!(err=?data, "Invalid dashboard query or queries"); + } + let _ = data?; + } + return Ok(()); } #[tokio::main] @@ -61,6 +75,14 @@ async fn main() -> anyhow::Result<()> { .expect("setting default subscriber failed"); let config = std::sync::Arc::new(dashboard::read_dashboard_list(args.config.as_path())?); + + if args.validate { + for dash in config.iter() { + validate(&dash).await?; + info!("All Queries successfully run against source"); + return Ok(()); + } + } let router = Router::new() // JSON api endpoints .nest("/js", routes::mk_js_routes(config.clone())) diff --git a/src/routes.rs b/src/routes.rs index 0ca1186..fe08553 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -24,7 +24,7 @@ use axum::{ use maud::{html, Markup}; use tracing::debug; -use crate::dashboard::{Dashboard, Graph, GraphSpan}; +use crate::dashboard::{Dashboard, Graph, GraphSpan, query_data}; use crate::query::{to_samples, QueryResult}; type Config = State>>; @@ -54,18 +54,7 @@ pub async fn graph_query( None } }; - let connections = graph.get_query_connections(&dash.span, &query_span); - let mut data = Vec::new(); - for conn in connections { - data.push(to_samples( - conn.get_results() - .await - .expect("Unable to get query results") - .data() - .clone(), - conn.meta, - )); - } + let data = query_data(graph, dash, query_span).await.expect("Unable to get query results"); Json(data) }