Compare commits

...

2 Commits

4 changed files with 90 additions and 21 deletions

View File

@ -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.

View File

@ -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<String>,
}
pub async fn query_data(graph: &Graph, dash: &Dashboard, query_span: Option<GraphSpan>) -> Result<Vec<QueryResult>> {
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<Duration> {
match parse_duration::parse(duration_string) {
Ok(d) => match Duration::from_std(d) {

View File

@ -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()))
@ -68,6 +90,7 @@ async fn main() -> anyhow::Result<()> {
.nest("/api", routes::mk_api_routes(config.clone()))
// HTMX ui component endpoints
.nest("/ui", routes::mk_ui_routes(config.clone()))
.route("/dash/:dash_idx", get(routes::dashboard_direct))
.route("/", get(routes::index).with_state(State(config.clone())))
.layer(TraceLayer::new_for_http())
.with_state(State(config.clone()));

View File

@ -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<Arc<Vec<Dashboard>>>;
@ -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)
}
@ -107,6 +96,10 @@ pub async fn graph_ui(
pub async fn dash_ui(State(config): State<Config>, Path(dash_idx): Path<usize>) -> Markup {
// TODO(zaphar): Should do better http error reporting here.
dash_elements(config, dash_idx)
}
fn dash_elements(config: State<Arc<Vec<Dashboard>>>, dash_idx: usize) -> maud::PreEscaped<String> {
let dash = config.get(dash_idx).expect("No such dashboard");
let graph_iter = dash
.graphs
@ -134,7 +127,7 @@ pub fn mk_ui_routes(config: Arc<Vec<Dashboard>>) -> Router<Config> {
)
}
pub async fn index(State(config): State<Config>) -> Markup {
async fn index_html(config: Config, dash_idx: Option<usize>) -> Markup {
html! {
html {
head {
@ -143,15 +136,26 @@ pub async fn index(State(config): State<Config>) -> Markup {
body {
script src="/js/plotly.js" { }
script src="/js/htmx.js" { }
script src="/js/lib.js" { }
script defer src="/js/lib.js" { }
link rel="stylesheet" href="/static/site.css" { }
(app(State(config.clone())).await)
(app(State(config.clone()), dash_idx).await)
}
}
}
}
pub async fn app(State(config): State<Config>) -> Markup {
pub async fn index(State(config): State<Config>) -> Markup {
index_html(config, None).await
}
pub async fn dashboard_direct(
State(config): State<Config>,
Path(dash_idx): Path<usize>,
) -> Markup {
index_html(config, Some(dash_idx)).await
}
fn render_index(config: State<Arc<Vec<Dashboard>>>, dash_idx: Option<usize>) -> Markup {
let titles = config
.iter()
.map(|d| d.title.clone())
@ -163,15 +167,23 @@ pub async fn app(State(config): State<Config>) -> Markup {
// Header menu
ul {
@for title in &titles {
li hx-get=(format!("/ui/dash/{}", title.0)) hx-target="#dashboard" { (title.1) }
li hx-push-url=(format!("/dash/{}", title.0)) hx-get=(format!("/ui/dash/{}", title.0)) hx-target="#dashboard" { (title.1) }
}
}
}
div class="flex-item-grow" id="dashboard" { }
div class="flex-item-grow" id="dashboard" {
@if let Some(dash_idx) = dash_idx {
(dash_elements(config, dash_idx))
}
}
}
}
}
pub async fn app(State(config): State<Config>, dash_idx: Option<usize>) -> Markup {
render_index(config, dash_idx)
}
pub fn javascript_response(content: &str) -> Response<String> {
Response::builder()
.header("Content-Type", "text/javascript")