mirror of
https://github.com/zaphar/Heracles.git
synced 2025-07-25 13:29:48 -04:00
Compare commits
2 Commits
835c120d4b
...
0af85229c2
Author | SHA1 | Date | |
---|---|---|---|
0af85229c2 | |||
8aa41eb68c |
18
examples/example_bad_dashboard.yaml
Normal file
18
examples/example_bad_dashboard.yaml
Normal 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.
|
@ -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) {
|
||||
|
25
src/main.rs
25
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()))
|
||||
@ -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()));
|
||||
|
@ -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")
|
||||
|
Loading…
x
Reference in New Issue
Block a user