From 08267f7727ecf30cb828a04a034180f6f03dd0fd Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Wed, 7 Feb 2024 19:18:30 -0600 Subject: [PATCH] feat: Huzzah! the graph renders! --- Cargo.toml | 1 + src/query.rs | 14 +++++++++----- src/routes.rs | 14 ++++++++++++-- static/lib.js | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 69 insertions(+), 7 deletions(-) create mode 100644 static/lib.js diff --git a/Cargo.toml b/Cargo.toml index f1902d5..ec36fd7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ anyhow = "1.0.79" async-io = "2.3.1" axum = { version = "0.7.4", features = [ "ws" ] } axum-macros = "0.4.1" +chrono = { version = "0.4.33", features = ["alloc", "std", "now"] } clap = { version = "4.4.18", features = ["derive"] } maud = { version = "0.26.0", features = ["axum"] } prometheus-http-query = "0.8.2" diff --git a/src/query.rs b/src/query.rs index 5b4452b..f36d23a 100644 --- a/src/query.rs +++ b/src/query.rs @@ -16,6 +16,7 @@ use std::collections::HashMap; use prometheus_http_query::{Client, response::{PromqlResult, Data}}; use serde::{Serialize, Deserialize}; use tracing::debug; +use chrono::prelude::*; pub struct QueryConn<'conn> { source: &'conn str, @@ -33,13 +34,16 @@ impl<'conn> QueryConn<'conn> { pub async fn get_results(&self) -> anyhow::Result { debug!("Getting results for query"); let client = Client::try_from(self.source)?; - Ok(client.query(self.query).get().await?) + let end = Utc::now().timestamp(); + let start = end - (60 * 10); + let step_resolution = 10 as f64; + Ok(client.query_range(self.query, start, end, step_resolution).get().await?) } } #[derive(Serialize, Deserialize)] pub struct DataPoint { - timesstamp: f64, + timestamp: f64, value: f64, } @@ -56,18 +60,18 @@ pub fn to_samples(data: Data) -> QueryResult { QueryResult::Series(range.drain(0..).map(|rv| { let (metric, mut samples) = rv.into_inner(); (metric, samples.drain(0..).map(|s| { - DataPoint { timesstamp: s.timestamp(), value: s.value() } + DataPoint { timestamp: s.timestamp(), value: s.value() } }).collect()) }).collect()) } Data::Vector(mut vector) => { QueryResult::Series(vector.drain(0..).map(|iv| { let (metric, sample) = iv.into_inner(); - (metric, vec![DataPoint { timesstamp: sample.timestamp(), value: sample.value() }]) + (metric, vec![DataPoint { timestamp: sample.timestamp(), value: sample.value() }]) }).collect()) } Data::Scalar(sample) => { - QueryResult::Scalar(DataPoint { timesstamp: sample.timestamp(), value: sample.value() }) + QueryResult::Scalar(DataPoint { timestamp: sample.timestamp(), value: sample.value() }) } } } diff --git a/src/routes.rs b/src/routes.rs index b577316..52d05bd 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -63,11 +63,14 @@ pub fn mk_api_routes(config: Arc>) -> Router { // TODO(jwall): This should probably be encapsulated in a web component? pub fn graph_component(dash_idx: usize, graph_idx: usize, graph: &Graph) -> Markup { let graph_id = format!("graph-{}-{}", dash_idx, graph_idx); + let graph_data_uri = format!("/api/dash/{}/graph/{}", dash_idx, graph_idx); // initialize the plot with Plotly.react // Update plot with Plotly.react which is more efficient let script = format!( - "var data = []; Plotly.react('{}', data, {{ width: 500, height: 500 }});", - graph_id + "var graph{graph_idx} = new Timeseries('{uri}', '{graph_id}'); graph{graph_idx}.updateGraph();", + uri = graph_data_uri, + graph_id = graph_id, + graph_idx = graph_idx, ); html!( div { @@ -130,6 +133,7 @@ pub async fn index(State(config): State) -> Markup { body { script src="/js/plotly.js" { } script src="/js/htmx.js" { } + script src="/js/lib.js" { } (app(State(config.clone())).await) } } @@ -163,6 +167,7 @@ pub fn javascript_response(content: &str) -> Response { .expect("Invalid javascript response") } +// TODO(jwall): Should probably hook in one of the axum directory serving crates here. pub async fn htmx() -> Response { javascript_response(include_str!("../static/htmx.min.js")) } @@ -171,9 +176,14 @@ pub async fn plotly() -> Response { javascript_response(include_str!("../static/plotly-2.27.0.min.js")) } +pub async fn lib() -> Response { + javascript_response(include_str!("../static/lib.js")) +} + pub fn mk_js_routes(config: Arc>) -> Router { Router::new() .route("/plotly.js", get(plotly)) + .route("/lib.js", get(lib)) .route("/htmx.js", get(htmx)) .with_state(State(config)) } diff --git a/static/lib.js b/static/lib.js new file mode 100644 index 0000000..3eb1899 --- /dev/null +++ b/static/lib.js @@ -0,0 +1,47 @@ + +class Timeseries { + #uri; + #title; + #targetEl; + //#width; + //#height; + + constructor(uri, targetEl, /** width, height **/) { + this.#uri = uri; + this.#targetEl = targetEl; + //this.#width = width; + //this.#height = height; + } + + async fetchData() { + const response = await fetch(this.#uri); + const data = await response.json(); + return data; + } + + async updateGraph() { + const data = await this.fetchData(); + if (data.Series) { + var traces = []; + for (const pair of data.Series) { + var trace = { + type: "scatter", + mode: "lines", + x: [], + y: [] + }; + //const labels = pair[0]; + const series = pair[1]; + for (const point of series) { + trace.x.push(point.timestamp); + trace.y.push(point.value); + } + traces.push(trace); + } + console.log("Traces: ", traces); + Plotly.react(this.#targetEl, traces, { width: 500, height: 500 }); + } else if (data.Scalar) { + // The graph should be a single value + } + } +}